Compare commits
7 commits
c5dd462a6e
...
1c21bd1259
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c21bd1259 | |||
| 5db14936e7 | |||
| f777b4c521 | |||
| b6502e1d8a | |||
| d756c8a6a2 | |||
| 5c2f631fdb | |||
| ce14169d6b |
79 changed files with 1775 additions and 157 deletions
|
|
@ -77,6 +77,20 @@ You can also right-click any title in a container to toggle mono mode.
|
||||||
In mono mode, scroll over the title bar to cycle between windows in the
|
In mono mode, scroll over the title bar to cycle between windows in the
|
||||||
container.
|
container.
|
||||||
|
|
||||||
|
## Autotiling
|
||||||
|
|
||||||
|
Autotiling makes newly tiled windows alternate split direction from the focused
|
||||||
|
tiled window. The first split uses the containing group direction, then later
|
||||||
|
windows wrap the focused tile in the opposite direction, producing a horizontal,
|
||||||
|
vertical, horizontal pattern as the layout grows.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[shortcuts]
|
||||||
|
alt-a = "toggle-autotile"
|
||||||
|
```
|
||||||
|
|
||||||
|
Manual grouping and split commands still use the direction you request.
|
||||||
|
|
||||||
## Fullscreen
|
## Fullscreen
|
||||||
|
|
||||||
Press `alt-u` (`toggle-fullscreen`) to make the focused window fill the entire
|
Press `alt-u` (`toggle-fullscreen`) to make the focused window fill the entire
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ pub enum SimpleCommand {
|
||||||
SetFloating(bool),
|
SetFloating(bool),
|
||||||
ToggleFullscreen,
|
ToggleFullscreen,
|
||||||
SetFullscreen(bool),
|
SetFullscreen(bool),
|
||||||
|
SendToScratchpad,
|
||||||
|
ToggleScratchpad,
|
||||||
|
CycleScratchpad,
|
||||||
Forward(bool),
|
Forward(bool),
|
||||||
EnableWindowManagement(bool),
|
EnableWindowManagement(bool),
|
||||||
SetFloatAboveFullscreen(bool),
|
SetFloatAboveFullscreen(bool),
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ pub struct Exec {
|
||||||
pub prog: String,
|
pub prog: String,
|
||||||
pub args: Vec<String>,
|
pub args: Vec<String>,
|
||||||
pub envs: Vec<(String, String)>,
|
pub envs: Vec<(String, String)>,
|
||||||
|
pub tag: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ pub use animations::{AnimationCurveConfig, Animations};
|
||||||
pub use command::{Exec, Status};
|
pub use command::{Exec, Status};
|
||||||
pub use input::InputMatch;
|
pub use input::InputMatch;
|
||||||
pub use keymap::ConfigKeymap;
|
pub use keymap::ConfigKeymap;
|
||||||
pub use model::{Action, ClientRule, Config, Input, InputMode, NamedAction, Shortcut, WindowRule};
|
pub use model::{
|
||||||
|
Action, ClientRule, Config, Input, InputMode, NamedAction, Scratchpad, Shortcut, WindowRule,
|
||||||
|
};
|
||||||
pub use options::{
|
pub use options::{
|
||||||
ColorManagement, Float, FocusHistory, Libei, RepeatRate, SimpleIm, Tearing, UiDrag, Vrr,
|
ColorManagement, Float, FocusHistory, Libei, RepeatRate, SimpleIm, Tearing, UiDrag, Vrr,
|
||||||
Xwayland,
|
Xwayland,
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,15 @@ pub enum Action {
|
||||||
MoveToWorkspace {
|
MoveToWorkspace {
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
|
SendToScratchpad {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
ToggleScratchpad {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
CycleScratchpad {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
Multi {
|
Multi {
|
||||||
actions: Vec<Action>,
|
actions: Vec<Action>,
|
||||||
},
|
},
|
||||||
|
|
@ -236,4 +245,12 @@ pub struct Config {
|
||||||
pub simple_im: Option<SimpleIm>,
|
pub simple_im: Option<SimpleIm>,
|
||||||
pub fallback_output_mode: Option<FallbackOutputMode>,
|
pub fallback_output_mode: Option<FallbackOutputMode>,
|
||||||
pub mouse_follows_focus: Option<bool>,
|
pub mouse_follows_focus: Option<bool>,
|
||||||
|
pub scratchpads: Vec<Scratchpad>,
|
||||||
|
pub autotile: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Scratchpad {
|
||||||
|
pub name: String,
|
||||||
|
pub exec: Option<Exec>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ pub struct ClientMatch {
|
||||||
pub comm_regex: Option<String>,
|
pub comm_regex: Option<String>,
|
||||||
pub exe: Option<String>,
|
pub exe: Option<String>,
|
||||||
pub exe_regex: Option<String>,
|
pub exe_regex: Option<String>,
|
||||||
|
pub tag: Option<String>,
|
||||||
|
pub tag_regex: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,15 @@ impl ConfigClient {
|
||||||
.drain()
|
.drain()
|
||||||
.map(|(a, b)| (a, b.into_raw_fd()))
|
.map(|(a, b)| (a, b.into_raw_fd()))
|
||||||
.collect();
|
.collect();
|
||||||
if fds.is_empty() {
|
if command.tag.is_some() {
|
||||||
|
self.send(&ClientMessage::Run3 {
|
||||||
|
prog: &command.prog,
|
||||||
|
args: command.args.clone(),
|
||||||
|
env,
|
||||||
|
fds,
|
||||||
|
tag: command.tag.as_deref(),
|
||||||
|
});
|
||||||
|
} else if fds.is_empty() {
|
||||||
self.send(&ClientMessage::Run {
|
self.send(&ClientMessage::Run {
|
||||||
prog: &command.prog,
|
prog: &command.prog,
|
||||||
args: command.args.clone(),
|
args: command.args.clone(),
|
||||||
|
|
@ -592,6 +600,22 @@ impl ConfigClient {
|
||||||
self.send(&ClientMessage::SetWindowWorkspace { window, workspace });
|
self.send(&ClientMessage::SetWindowWorkspace { window, workspace });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn seat_send_to_scratchpad(&self, seat: Seat, name: &str) {
|
||||||
|
self.send(&ClientMessage::SeatSendToScratchpad { seat, name });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seat_toggle_scratchpad(&self, seat: Seat, name: &str) {
|
||||||
|
self.send(&ClientMessage::SeatToggleScratchpad { seat, name });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seat_cycle_scratchpad(&self, seat: Seat, name: &str) {
|
||||||
|
self.send(&ClientMessage::SeatCycleScratchpad { seat, name });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn window_send_to_scratchpad(&self, window: Window, name: &str) {
|
||||||
|
self.send(&ClientMessage::WindowSendToScratchpad { window, name });
|
||||||
|
}
|
||||||
|
|
||||||
pub fn seat_split(&self, seat: Seat) -> Axis {
|
pub fn seat_split(&self, seat: Seat) -> Axis {
|
||||||
let res = self.send_with_response(&ClientMessage::GetSeatSplit { seat });
|
let res = self.send_with_response(&ClientMessage::GetSeatSplit { seat });
|
||||||
get_response!(res, Axis::Horizontal, GetSplit { axis });
|
get_response!(res, Axis::Horizontal, GetSplit { axis });
|
||||||
|
|
@ -1798,6 +1822,8 @@ impl ConfigClient {
|
||||||
ClientCriterion::CommRegex(t) => string!(t, Comm, true),
|
ClientCriterion::CommRegex(t) => string!(t, Comm, true),
|
||||||
ClientCriterion::Exe(t) => string!(t, Exe, false),
|
ClientCriterion::Exe(t) => string!(t, Exe, false),
|
||||||
ClientCriterion::ExeRegex(t) => string!(t, Exe, true),
|
ClientCriterion::ExeRegex(t) => string!(t, Exe, true),
|
||||||
|
ClientCriterion::Tag(t) => string!(t, Tag, false),
|
||||||
|
ClientCriterion::TagRegex(t) => string!(t, Tag, true),
|
||||||
};
|
};
|
||||||
let res = self.send_with_response(&ClientMessage::CreateClientMatcher { criterion });
|
let res = self.send_with_response(&ClientMessage::CreateClientMatcher { criterion });
|
||||||
get_response!(
|
get_response!(
|
||||||
|
|
@ -2016,6 +2042,12 @@ impl ConfigClient {
|
||||||
self.send(&ClientMessage::SetAutotile { enabled });
|
self.send(&ClientMessage::SetAutotile { enabled });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_autotile(&self) -> bool {
|
||||||
|
let res = self.send_with_response(&ClientMessage::GetAutotile);
|
||||||
|
get_response!(res, false, GetAutotile { enabled });
|
||||||
|
enabled
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_tab_title_align(&self, align: u32) {
|
pub fn set_tab_title_align(&self, align: u32) {
|
||||||
self.send(&ClientMessage::SetTabTitleAlign { align });
|
self.send(&ClientMessage::SetTabTitleAlign { align });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,10 @@ pub enum ClientCriterion<'a> {
|
||||||
Exe(&'a str),
|
Exe(&'a str),
|
||||||
/// Matches the `/proc/pid/exe` of the client with a regular expression.
|
/// Matches the `/proc/pid/exe` of the client with a regular expression.
|
||||||
ExeRegex(&'a str),
|
ExeRegex(&'a str),
|
||||||
|
/// Matches the tag of the client verbatim.
|
||||||
|
Tag(&'a str),
|
||||||
|
/// Matches the tag of the client with a regular expression.
|
||||||
|
TagRegex(&'a str),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientCriterion<'_> {
|
impl ClientCriterion<'_> {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ pub struct Command {
|
||||||
pub(crate) args: Vec<String>,
|
pub(crate) args: Vec<String>,
|
||||||
pub(crate) env: HashMap<String, String>,
|
pub(crate) env: HashMap<String, String>,
|
||||||
pub(crate) fds: RefCell<HashMap<i32, OwnedFd>>,
|
pub(crate) fds: RefCell<HashMap<i32, OwnedFd>>,
|
||||||
|
pub(crate) tag: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
|
|
@ -37,6 +38,7 @@ impl Command {
|
||||||
args: vec![],
|
args: vec![],
|
||||||
env: Default::default(),
|
env: Default::default(),
|
||||||
fds: Default::default(),
|
fds: Default::default(),
|
||||||
|
tag: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,6 +84,12 @@ impl Command {
|
||||||
self.fd(2, fd)
|
self.fd(2, fd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds a tag to Wayland connections created by the spawned command.
|
||||||
|
pub fn tag(&mut self, tag: &str) -> &mut Self {
|
||||||
|
self.tag = Some(tag.to_owned());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Executes the command.
|
/// Executes the command.
|
||||||
///
|
///
|
||||||
/// This consumes all attached file descriptors.
|
/// This consumes all attached file descriptors.
|
||||||
|
|
|
||||||
|
|
@ -466,6 +466,33 @@ impl Seat {
|
||||||
get!().set_seat_workspace(self, workspace)
|
get!().set_seat_workspace(self, workspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sends the currently focused window to a scratchpad.
|
||||||
|
///
|
||||||
|
/// Use an empty string for the default scratchpad.
|
||||||
|
pub fn send_to_scratchpad(self, name: &str) {
|
||||||
|
get!().seat_send_to_scratchpad(self, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggles a scratchpad.
|
||||||
|
///
|
||||||
|
/// If the scratchpad has a visible window, that window is hidden. Otherwise, the
|
||||||
|
/// most recently hidden window in the scratchpad is shown on the current workspace.
|
||||||
|
/// Scratchpad windows are always shown floating.
|
||||||
|
/// Use an empty string for the default scratchpad.
|
||||||
|
pub fn toggle_scratchpad(self, name: &str) {
|
||||||
|
get!().seat_toggle_scratchpad(self, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cycles through the windows of a scratchpad, one at a time.
|
||||||
|
///
|
||||||
|
/// With nothing shown, the first window is brought up; each further invocation
|
||||||
|
/// hides the current window and shows the next; after the last window the
|
||||||
|
/// scratchpad is hidden again. Scratchpad windows are always shown floating.
|
||||||
|
/// Use an empty string for the default scratchpad.
|
||||||
|
pub fn cycle_scratchpad(self, name: &str) {
|
||||||
|
get!().seat_cycle_scratchpad(self, name)
|
||||||
|
}
|
||||||
|
|
||||||
/// Toggles whether the currently focused window is fullscreen.
|
/// Toggles whether the currently focused window is fullscreen.
|
||||||
pub fn toggle_fullscreen(self) {
|
pub fn toggle_fullscreen(self) {
|
||||||
let c = get!();
|
let c = get!();
|
||||||
|
|
|
||||||
|
|
@ -437,14 +437,21 @@ pub fn get_corner_radius() -> f32 {
|
||||||
|
|
||||||
/// Enables or disables autotiling.
|
/// Enables or disables autotiling.
|
||||||
///
|
///
|
||||||
/// When enabled, new windows are automatically placed in a perpendicular
|
/// When enabled, newly tiled windows alternate split orientation from the
|
||||||
/// sub-container if the predicted body would be narrower than tall (or vice versa).
|
/// focused tiled window: the first split uses the containing group's direction,
|
||||||
|
/// then subsequent splits wrap the focused window in the perpendicular
|
||||||
|
/// direction.
|
||||||
///
|
///
|
||||||
/// The default is `false`.
|
/// The default is `false`.
|
||||||
pub fn set_autotile(enabled: bool) {
|
pub fn set_autotile(enabled: bool) {
|
||||||
get!().set_autotile(enabled)
|
get!().set_autotile(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether autotiling is enabled.
|
||||||
|
pub fn get_autotile() -> bool {
|
||||||
|
get!(false).get_autotile()
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the horizontal alignment of title text within tab buttons.
|
/// Sets the horizontal alignment of title text within tab buttons.
|
||||||
///
|
///
|
||||||
/// - `"start"` — left-aligned (default)
|
/// - `"start"` — left-aligned (default)
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ pub enum ClientCriterionStringField {
|
||||||
SandboxInstanceId,
|
SandboxInstanceId,
|
||||||
Comm,
|
Comm,
|
||||||
Exe,
|
Exe,
|
||||||
|
Tag,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
|
|
@ -411,6 +412,18 @@ pub enum ClientMessage<'a> {
|
||||||
seat: Seat,
|
seat: Seat,
|
||||||
workspace: Workspace,
|
workspace: Workspace,
|
||||||
},
|
},
|
||||||
|
SeatSendToScratchpad {
|
||||||
|
seat: Seat,
|
||||||
|
name: &'a str,
|
||||||
|
},
|
||||||
|
SeatToggleScratchpad {
|
||||||
|
seat: Seat,
|
||||||
|
name: &'a str,
|
||||||
|
},
|
||||||
|
SeatCycleScratchpad {
|
||||||
|
seat: Seat,
|
||||||
|
name: &'a str,
|
||||||
|
},
|
||||||
GetTimer {
|
GetTimer {
|
||||||
name: &'a str,
|
name: &'a str,
|
||||||
},
|
},
|
||||||
|
|
@ -557,6 +570,13 @@ pub enum ClientMessage<'a> {
|
||||||
env: Vec<(String, String)>,
|
env: Vec<(String, String)>,
|
||||||
fds: Vec<(i32, i32)>,
|
fds: Vec<(i32, i32)>,
|
||||||
},
|
},
|
||||||
|
Run3 {
|
||||||
|
prog: &'a str,
|
||||||
|
args: Vec<String>,
|
||||||
|
env: Vec<(String, String)>,
|
||||||
|
fds: Vec<(i32, i32)>,
|
||||||
|
tag: Option<&'a str>,
|
||||||
|
},
|
||||||
DisableDefaultSeat,
|
DisableDefaultSeat,
|
||||||
DestroyKeymap {
|
DestroyKeymap {
|
||||||
keymap: Keymap,
|
keymap: Keymap,
|
||||||
|
|
@ -817,6 +837,10 @@ pub enum ClientMessage<'a> {
|
||||||
window: Window,
|
window: Window,
|
||||||
workspace: Workspace,
|
workspace: Workspace,
|
||||||
},
|
},
|
||||||
|
WindowSendToScratchpad {
|
||||||
|
window: Window,
|
||||||
|
name: &'a str,
|
||||||
|
},
|
||||||
SetWindowFullscreen {
|
SetWindowFullscreen {
|
||||||
window: Window,
|
window: Window,
|
||||||
fullscreen: bool,
|
fullscreen: bool,
|
||||||
|
|
@ -1038,6 +1062,7 @@ pub enum ClientMessage<'a> {
|
||||||
SetAutotile {
|
SetAutotile {
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
},
|
},
|
||||||
|
GetAutotile,
|
||||||
SetTabTitleAlign {
|
SetTabTitleAlign {
|
||||||
align: u32,
|
align: u32,
|
||||||
},
|
},
|
||||||
|
|
@ -1301,6 +1326,9 @@ pub enum Response {
|
||||||
GetCornerRadius {
|
GetCornerRadius {
|
||||||
radius: f32,
|
radius: f32,
|
||||||
},
|
},
|
||||||
|
GetAutotile {
|
||||||
|
enabled: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,13 @@ impl Window {
|
||||||
get!().set_window_workspace(self, workspace)
|
get!().set_window_workspace(self, workspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sends the window to a scratchpad.
|
||||||
|
///
|
||||||
|
/// Use an empty string for the default scratchpad.
|
||||||
|
pub fn send_to_scratchpad(self, name: &str) {
|
||||||
|
get!().window_send_to_scratchpad(self, name)
|
||||||
|
}
|
||||||
|
|
||||||
/// Toggles whether the currently focused window is fullscreen.
|
/// Toggles whether the currently focused window is fullscreen.
|
||||||
pub fn toggle_fullscreen(self) {
|
pub fn toggle_fullscreen(self) {
|
||||||
self.set_fullscreen(!self.fullscreen())
|
self.set_fullscreen(!self.fullscreen())
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,8 @@ pub use jay_config_schema::{
|
||||||
Action, AnimationCurveConfig, Animations, ClientMatch, ClientRule, ColorManagement, Config,
|
Action, AnimationCurveConfig, Animations, ClientMatch, ClientRule, ColorManagement, Config,
|
||||||
ConfigConnector, ConfigDrmDevice, ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Float,
|
ConfigConnector, ConfigDrmDevice, ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Float,
|
||||||
FocusHistory, GenericMatch, Input, InputMatch, InputMode, Libei, MatchExactly, Mode,
|
FocusHistory, GenericMatch, Input, InputMatch, InputMode, Libei, MatchExactly, Mode,
|
||||||
NamedAction, Output, OutputMatch, RepeatRate, Shortcut, SimpleCommand, SimpleIm, Status,
|
NamedAction, Output, OutputMatch, RepeatRate, Scratchpad, Shortcut, SimpleCommand, SimpleIm,
|
||||||
Tearing, Theme, UiDrag, Vrr, WindowMatch, WindowRule, Xwayland,
|
Status, Tearing, Theme, UiDrag, Vrr, WindowMatch, WindowRule, Xwayland,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ pub mod modified_keysym;
|
||||||
mod output;
|
mod output;
|
||||||
mod output_match;
|
mod output_match;
|
||||||
mod repeat_rate;
|
mod repeat_rate;
|
||||||
|
mod scratchpad;
|
||||||
pub mod shortcuts;
|
pub mod shortcuts;
|
||||||
mod simple_im;
|
mod simple_im;
|
||||||
mod status;
|
mod status;
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,9 @@ impl ActionParser<'_> {
|
||||||
"toggle-fullscreen" => ToggleFullscreen,
|
"toggle-fullscreen" => ToggleFullscreen,
|
||||||
"enter-fullscreen" => SetFullscreen(true),
|
"enter-fullscreen" => SetFullscreen(true),
|
||||||
"exit-fullscreen" => SetFullscreen(false),
|
"exit-fullscreen" => SetFullscreen(false),
|
||||||
|
"send-to-scratchpad" => SendToScratchpad,
|
||||||
|
"toggle-scratchpad" => ToggleScratchpad,
|
||||||
|
"cycle-scratchpad" => CycleScratchpad,
|
||||||
"focus-parent" => FocusParent,
|
"focus-parent" => FocusParent,
|
||||||
"close" => Close,
|
"close" => Close,
|
||||||
"disable-pointer-constraint" => DisablePointerConstraint,
|
"disable-pointer-constraint" => DisablePointerConstraint,
|
||||||
|
|
@ -221,6 +224,33 @@ impl ActionParser<'_> {
|
||||||
Ok(Action::MoveToWorkspace { name })
|
Ok(Action::MoveToWorkspace { name })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_send_to_scratchpad(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
|
||||||
|
let name = ext
|
||||||
|
.extract(opt(str("name")))?
|
||||||
|
.map(|name| name.value)
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
Ok(Action::SendToScratchpad { name })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_toggle_scratchpad(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
|
||||||
|
let name = ext
|
||||||
|
.extract(opt(str("name")))?
|
||||||
|
.map(|name| name.value)
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
Ok(Action::ToggleScratchpad { name })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_cycle_scratchpad(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
|
||||||
|
let name = ext
|
||||||
|
.extract(opt(str("name")))?
|
||||||
|
.map(|name| name.value)
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
Ok(Action::CycleScratchpad { name })
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_configure_connector(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
|
fn parse_configure_connector(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
|
||||||
let con = ext
|
let con = ext
|
||||||
.extract(val("connector"))?
|
.extract(val("connector"))?
|
||||||
|
|
@ -550,6 +580,9 @@ impl Parser for ActionParser<'_> {
|
||||||
"switch-to-vt" => self.parse_switch_to_vt(&mut ext),
|
"switch-to-vt" => self.parse_switch_to_vt(&mut ext),
|
||||||
"show-workspace" => self.parse_show_workspace(&mut ext),
|
"show-workspace" => self.parse_show_workspace(&mut ext),
|
||||||
"move-to-workspace" => self.parse_move_to_workspace(&mut ext),
|
"move-to-workspace" => self.parse_move_to_workspace(&mut ext),
|
||||||
|
"send-to-scratchpad" => self.parse_send_to_scratchpad(&mut ext),
|
||||||
|
"toggle-scratchpad" => self.parse_toggle_scratchpad(&mut ext),
|
||||||
|
"cycle-scratchpad" => self.parse_cycle_scratchpad(&mut ext),
|
||||||
"configure-connector" => self.parse_configure_connector(&mut ext),
|
"configure-connector" => self.parse_configure_connector(&mut ext),
|
||||||
"configure-input" => self.parse_configure_input(&mut ext),
|
"configure-input" => self.parse_configure_input(&mut ext),
|
||||||
"configure-output" => self.parse_configure_output(&mut ext),
|
"configure-output" => self.parse_configure_output(&mut ext),
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ impl Parser for ClientMatchParser<'_> {
|
||||||
exe,
|
exe,
|
||||||
exe_regex,
|
exe_regex,
|
||||||
),
|
),
|
||||||
(is_xwayland,),
|
(is_xwayland, tag, tag_regex),
|
||||||
) = ext.extract((
|
) = ext.extract((
|
||||||
(
|
(
|
||||||
opt(str("name")),
|
opt(str("name")),
|
||||||
|
|
@ -83,7 +83,11 @@ impl Parser for ClientMatchParser<'_> {
|
||||||
opt(str("exe")),
|
opt(str("exe")),
|
||||||
opt(str("exe-regex")),
|
opt(str("exe-regex")),
|
||||||
),
|
),
|
||||||
(opt(bol("is-xwayland")),),
|
(
|
||||||
|
opt(bol("is-xwayland")),
|
||||||
|
opt(str("tag")),
|
||||||
|
opt(str("tag-regex")),
|
||||||
|
),
|
||||||
))?;
|
))?;
|
||||||
let mut not = None;
|
let mut not = None;
|
||||||
if let Some(value) = not_val {
|
if let Some(value) = not_val {
|
||||||
|
|
@ -130,6 +134,8 @@ impl Parser for ClientMatchParser<'_> {
|
||||||
comm_regex: comm_regex.despan_into(),
|
comm_regex: comm_regex.despan_into(),
|
||||||
exe: exe.despan_into(),
|
exe: exe.despan_into(),
|
||||||
exe_regex: exe_regex.despan_into(),
|
exe_regex: exe_regex.despan_into(),
|
||||||
|
tag: tag.despan_into(),
|
||||||
|
tag_regex: tag_regex.despan_into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ use {
|
||||||
log_level::LogLevelParser,
|
log_level::LogLevelParser,
|
||||||
output::OutputsParser,
|
output::OutputsParser,
|
||||||
repeat_rate::RepeatRateParser,
|
repeat_rate::RepeatRateParser,
|
||||||
|
scratchpad::ScratchpadsParser,
|
||||||
shortcuts::{
|
shortcuts::{
|
||||||
ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError,
|
ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError,
|
||||||
parse_modified_keysym_str,
|
parse_modified_keysym_str,
|
||||||
|
|
@ -156,6 +157,7 @@ impl Parser for ConfigParser<'_> {
|
||||||
mouse_follows_focus,
|
mouse_follows_focus,
|
||||||
animations_val,
|
animations_val,
|
||||||
),
|
),
|
||||||
|
(scratchpads_val, autotile),
|
||||||
) = ext.extract((
|
) = ext.extract((
|
||||||
(
|
(
|
||||||
opt(val("keymap")),
|
opt(val("keymap")),
|
||||||
|
|
@ -217,6 +219,7 @@ impl Parser for ConfigParser<'_> {
|
||||||
recover(opt(bol("unstable-mouse-follows-focus"))),
|
recover(opt(bol("unstable-mouse-follows-focus"))),
|
||||||
opt(val("animations")),
|
opt(val("animations")),
|
||||||
),
|
),
|
||||||
|
(opt(val("scratchpads")), recover(opt(bol("autotile")))),
|
||||||
))?;
|
))?;
|
||||||
let mut keymap = None;
|
let mut keymap = None;
|
||||||
if let Some(value) = keymap_val {
|
if let Some(value) = keymap_val {
|
||||||
|
|
@ -572,6 +575,13 @@ impl Parser for ConfigParser<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let mut scratchpads = vec![];
|
||||||
|
if let Some(value) = scratchpads_val {
|
||||||
|
match value.parse(&mut ScratchpadsParser(self.0)) {
|
||||||
|
Ok(v) => scratchpads = v,
|
||||||
|
Err(e) => log::warn!("Could not parse the scratchpads: {}", self.0.error(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
keymap,
|
keymap,
|
||||||
repeat_rate,
|
repeat_rate,
|
||||||
|
|
@ -624,6 +634,8 @@ impl Parser for ConfigParser<'_> {
|
||||||
simple_im,
|
simple_im,
|
||||||
fallback_output_mode,
|
fallback_output_mode,
|
||||||
mouse_follows_focus: mouse_follows_focus.despan(),
|
mouse_follows_focus: mouse_follows_focus.despan(),
|
||||||
|
scratchpads,
|
||||||
|
autotile: autotile.despan(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
jay_toml::{
|
jay_toml::{
|
||||||
toml_span::{Span, Spanned, SpannedExt},
|
toml_span::{DespanExt, Span, Spanned, SpannedExt},
|
||||||
toml_value::Value,
|
toml_value::Value,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -52,6 +52,7 @@ impl Parser for ExecParser<'_> {
|
||||||
prog: string.to_string(),
|
prog: string.to_string(),
|
||||||
args: vec![],
|
args: vec![],
|
||||||
envs: vec![],
|
envs: vec![],
|
||||||
|
tag: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,6 +69,7 @@ impl Parser for ExecParser<'_> {
|
||||||
prog,
|
prog,
|
||||||
args,
|
args,
|
||||||
envs: vec![],
|
envs: vec![],
|
||||||
|
tag: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,11 +79,12 @@ impl Parser for ExecParser<'_> {
|
||||||
table: &IndexMap<Spanned<String>, Spanned<Value>>,
|
table: &IndexMap<Spanned<String>, Spanned<Value>>,
|
||||||
) -> ParseResult<Self> {
|
) -> ParseResult<Self> {
|
||||||
let mut ext = Extractor::new(self.0, span, table);
|
let mut ext = Extractor::new(self.0, span, table);
|
||||||
let (prog_opt, shell_opt, args_val, envs_val) = ext.extract((
|
let (prog_opt, shell_opt, args_val, envs_val, tag) = ext.extract((
|
||||||
opt(str("prog")),
|
opt(str("prog")),
|
||||||
opt(str("shell")),
|
opt(str("shell")),
|
||||||
opt(arr("args")),
|
opt(arr("args")),
|
||||||
opt(val("env")),
|
opt(val("env")),
|
||||||
|
opt(str("tag")),
|
||||||
))?;
|
))?;
|
||||||
let prog;
|
let prog;
|
||||||
let mut args = vec![];
|
let mut args = vec![];
|
||||||
|
|
@ -113,6 +116,7 @@ impl Parser for ExecParser<'_> {
|
||||||
prog,
|
prog,
|
||||||
args,
|
args,
|
||||||
envs,
|
envs,
|
||||||
|
tag: tag.despan_into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
87
crates/toml-config/src/config/parsers/scratchpad.rs
Normal file
87
crates/toml-config/src/config/parsers/scratchpad.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
use {
|
||||||
|
crate::{
|
||||||
|
config::{
|
||||||
|
Scratchpad,
|
||||||
|
context::Context,
|
||||||
|
extractor::{Extractor, ExtractorError, opt, str, val},
|
||||||
|
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
|
||||||
|
parsers::exec::{ExecParser, ExecParserError},
|
||||||
|
},
|
||||||
|
jay_toml::{
|
||||||
|
toml_span::{Span, Spanned},
|
||||||
|
toml_value::Value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
indexmap::IndexMap,
|
||||||
|
thiserror::Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ScratchpadParserError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Expected(#[from] UnexpectedDataType),
|
||||||
|
#[error(transparent)]
|
||||||
|
Extract(#[from] ExtractorError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Exec(#[from] ExecParserError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ScratchpadParser<'a>(pub &'a Context<'a>);
|
||||||
|
|
||||||
|
impl Parser for ScratchpadParser<'_> {
|
||||||
|
type Value = Scratchpad;
|
||||||
|
type Error = ScratchpadParserError;
|
||||||
|
const EXPECTED: &'static [DataType] = &[DataType::Table];
|
||||||
|
|
||||||
|
fn parse_table(
|
||||||
|
&mut self,
|
||||||
|
span: Span,
|
||||||
|
table: &IndexMap<Spanned<String>, Spanned<Value>>,
|
||||||
|
) -> ParseResult<Self> {
|
||||||
|
let mut ext = Extractor::new(self.0, span, table);
|
||||||
|
let (name, exec_val) = ext.extract((str("name"), opt(val("exec"))))?;
|
||||||
|
let exec = match exec_val {
|
||||||
|
None => None,
|
||||||
|
Some(e) => Some(e.parse_map(&mut ExecParser(self.0))?),
|
||||||
|
};
|
||||||
|
Ok(Scratchpad {
|
||||||
|
name: name.value.to_string(),
|
||||||
|
exec,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ScratchpadsParser<'a>(pub &'a Context<'a>);
|
||||||
|
|
||||||
|
impl Parser for ScratchpadsParser<'_> {
|
||||||
|
type Value = Vec<Scratchpad>;
|
||||||
|
type Error = ScratchpadParserError;
|
||||||
|
const EXPECTED: &'static [DataType] = &[DataType::Table, DataType::Array];
|
||||||
|
|
||||||
|
fn parse_array(&mut self, _span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
|
||||||
|
let mut res = vec![];
|
||||||
|
for el in array {
|
||||||
|
match el.parse(&mut ScratchpadParser(self.0)) {
|
||||||
|
Ok(o) => res.push(o),
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Could not parse scratchpad: {}", self.0.error(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_table(
|
||||||
|
&mut self,
|
||||||
|
span: Span,
|
||||||
|
table: &IndexMap<Spanned<String>, Spanned<Value>>,
|
||||||
|
) -> ParseResult<Self> {
|
||||||
|
log::warn!(
|
||||||
|
"`scratchpads` value should be an array: {}",
|
||||||
|
self.0.error3(span)
|
||||||
|
);
|
||||||
|
ScratchpadParser(self.0)
|
||||||
|
.parse_table(span, table)
|
||||||
|
.map(|v| vec![v])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,9 +14,10 @@ pub(crate) use jay_toml;
|
||||||
use {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
config::{
|
config::{
|
||||||
Action, AnimationCurveConfig, ClientRule, Config, ConfigConnector, ConfigDrmDevice,
|
Action, AnimationCurveConfig, ClientMatch, ClientRule, Config, ConfigConnector,
|
||||||
ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output,
|
ConfigDrmDevice, ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Input,
|
||||||
OutputMatch, SimpleCommand, Status, Theme, WindowRule, parse_config,
|
InputMatch, Output, OutputMatch, SimpleCommand, Status, Theme, WindowMatch,
|
||||||
|
WindowRule, parse_config,
|
||||||
},
|
},
|
||||||
rules::{MatcherTemp, RuleMapper},
|
rules::{MatcherTemp, RuleMapper},
|
||||||
shortcuts::ModeState,
|
shortcuts::ModeState,
|
||||||
|
|
@ -28,7 +29,7 @@ use {
|
||||||
client::Client,
|
client::Client,
|
||||||
config_dir,
|
config_dir,
|
||||||
exec::{Command, set_env, unset_env},
|
exec::{Command, set_env, unset_env},
|
||||||
get_workspace,
|
get_autotile, get_workspace,
|
||||||
input::{
|
input::{
|
||||||
FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, capability::CAP_SWITCH,
|
FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, capability::CAP_SWITCH,
|
||||||
get_seat, input_devices, on_input_device_removed, on_new_input_device,
|
get_seat, input_devices, on_input_device_removed, on_new_input_device,
|
||||||
|
|
@ -41,11 +42,11 @@ use {
|
||||||
on_devices_enumerated, on_idle, on_unload, quit, set_animation_cubic_bezier,
|
on_devices_enumerated, on_idle, on_unload, quit, set_animation_cubic_bezier,
|
||||||
set_animation_curve, set_animation_duration_ms, set_animation_style,
|
set_animation_curve, set_animation_duration_ms, set_animation_style,
|
||||||
set_animations_enabled, set_autotile, set_color_management_enabled, set_corner_radius,
|
set_animations_enabled, set_autotile, set_color_management_enabled, set_corner_radius,
|
||||||
set_default_workspace_capture,
|
set_default_workspace_capture, set_explicit_sync_enabled, set_float_above_fullscreen,
|
||||||
set_explicit_sync_enabled, set_float_above_fullscreen, set_floating_titles, set_idle,
|
set_floating_titles, set_idle, set_idle_grace_period, set_key_press_enables_dpms,
|
||||||
set_idle_grace_period, set_key_press_enables_dpms, set_middle_click_paste_enabled,
|
set_middle_click_paste_enabled, set_mouse_move_enables_dpms, set_show_bar,
|
||||||
set_mouse_move_enables_dpms, set_show_bar, set_show_float_pin_icon, set_show_titles,
|
set_show_float_pin_icon, set_show_titles, set_tab_title_align, set_ui_drag_enabled,
|
||||||
set_tab_title_align, set_ui_drag_enabled, set_ui_drag_threshold,
|
set_ui_drag_threshold,
|
||||||
status::{set_i3bar_separator, set_status, set_status_command, unset_status_command},
|
status::{set_i3bar_separator, set_status, set_status_command, unset_status_command},
|
||||||
switch_to_vt,
|
switch_to_vt,
|
||||||
tasks::{self, JoinHandle},
|
tasks::{self, JoinHandle},
|
||||||
|
|
@ -185,6 +186,9 @@ impl ActionExt for Action {
|
||||||
SimpleCommand::Move(dir) => window_or_seat!(s, s.move_(dir)),
|
SimpleCommand::Move(dir) => window_or_seat!(s, s.move_(dir)),
|
||||||
SimpleCommand::ToggleFullscreen => window_or_seat!(s, s.toggle_fullscreen()),
|
SimpleCommand::ToggleFullscreen => window_or_seat!(s, s.toggle_fullscreen()),
|
||||||
SimpleCommand::SetFullscreen(b) => window_or_seat!(s, s.set_fullscreen(b)),
|
SimpleCommand::SetFullscreen(b) => window_or_seat!(s, s.set_fullscreen(b)),
|
||||||
|
SimpleCommand::SendToScratchpad => window_or_seat!(s, s.send_to_scratchpad("")),
|
||||||
|
SimpleCommand::ToggleScratchpad => b.new(move || s.toggle_scratchpad("")),
|
||||||
|
SimpleCommand::CycleScratchpad => b.new(move || s.cycle_scratchpad("")),
|
||||||
SimpleCommand::FocusParent => b.new(move || s.focus_parent()),
|
SimpleCommand::FocusParent => b.new(move || s.focus_parent()),
|
||||||
SimpleCommand::Close => window_or_seat!(s, s.close()),
|
SimpleCommand::Close => window_or_seat!(s, s.close()),
|
||||||
SimpleCommand::DisablePointerConstraint => {
|
SimpleCommand::DisablePointerConstraint => {
|
||||||
|
|
@ -280,12 +284,7 @@ impl ActionExt for Action {
|
||||||
SimpleCommand::MoveTabLeft => b.new(move || s.move_tab(false)),
|
SimpleCommand::MoveTabLeft => b.new(move || s.move_tab(false)),
|
||||||
SimpleCommand::MoveTabRight => b.new(move || s.move_tab(true)),
|
SimpleCommand::MoveTabRight => b.new(move || s.move_tab(true)),
|
||||||
SimpleCommand::SetAutotile(enabled) => b.new(move || set_autotile(enabled)),
|
SimpleCommand::SetAutotile(enabled) => b.new(move || set_autotile(enabled)),
|
||||||
SimpleCommand::ToggleAutotile => {
|
SimpleCommand::ToggleAutotile => b.new(move || set_autotile(!get_autotile())),
|
||||||
b.new(move || {
|
|
||||||
// Toggle not directly supported; set to true
|
|
||||||
set_autotile(true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Action::Multi { actions } => {
|
Action::Multi { actions } => {
|
||||||
let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();
|
let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();
|
||||||
|
|
@ -322,6 +321,9 @@ impl ActionExt for Action {
|
||||||
let workspace = get_workspace(&name);
|
let workspace = get_workspace(&name);
|
||||||
window_or_seat!(s, s.set_workspace(workspace))
|
window_or_seat!(s, s.set_workspace(workspace))
|
||||||
}
|
}
|
||||||
|
Action::SendToScratchpad { name } => window_or_seat!(s, s.send_to_scratchpad(&name)),
|
||||||
|
Action::ToggleScratchpad { name } => b.new(move || s.toggle_scratchpad(&name)),
|
||||||
|
Action::CycleScratchpad { name } => b.new(move || s.cycle_scratchpad(&name)),
|
||||||
Action::ConfigureConnector { con } => b.new(move || {
|
Action::ConfigureConnector { con } => b.new(move || {
|
||||||
for c in connectors() {
|
for c in connectors() {
|
||||||
if con.match_.matches(c) {
|
if con.match_.matches(c) {
|
||||||
|
|
@ -1526,6 +1528,46 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
|
||||||
window: Default::default(),
|
window: Default::default(),
|
||||||
});
|
});
|
||||||
state.clear_modes_after_reload();
|
state.clear_modes_after_reload();
|
||||||
|
// Desugar `[[scratchpads]]` into spawn-on-graphics-init plus an internal
|
||||||
|
// window rule that parks the spawned window. Each spawned process gets a
|
||||||
|
// unique tag so only its own windows are captured, never other windows of
|
||||||
|
// the same application.
|
||||||
|
if !config.scratchpads.is_empty() {
|
||||||
|
let mut spawn_actions = vec![];
|
||||||
|
for (i, sp) in config.scratchpads.drain(..).enumerate() {
|
||||||
|
let Some(mut exec) = sp.exec else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let tag = exec
|
||||||
|
.tag
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| format!("__scratchpad.{i}.{}", sp.name));
|
||||||
|
exec.tag = Some(tag.clone());
|
||||||
|
spawn_actions.push(Action::Exec { exec });
|
||||||
|
config.window_rules.push(WindowRule {
|
||||||
|
name: None,
|
||||||
|
match_: WindowMatch {
|
||||||
|
client: Some(ClientMatch {
|
||||||
|
tag: Some(tag),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
action: Some(Action::SendToScratchpad { name: sp.name }),
|
||||||
|
latch: None,
|
||||||
|
auto_focus: None,
|
||||||
|
initial_tile_state: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !spawn_actions.is_empty() {
|
||||||
|
let mut actions = Vec::with_capacity(spawn_actions.len() + 1);
|
||||||
|
if let Some(existing) = config.on_graphics_initialized.take() {
|
||||||
|
actions.push(existing);
|
||||||
|
}
|
||||||
|
actions.extend(spawn_actions);
|
||||||
|
config.on_graphics_initialized = Some(Action::Multi { actions });
|
||||||
|
}
|
||||||
|
}
|
||||||
let (client_rules, client_rule_mapper) = state.create_rules(&config.client_rules);
|
let (client_rules, client_rule_mapper) = state.create_rules(&config.client_rules);
|
||||||
persistent.client_rules.set(client_rules);
|
persistent.client_rules.set(client_rules);
|
||||||
*state.persistent.client_rule_mapper.borrow_mut() = Some(client_rule_mapper);
|
*state.persistent.client_rule_mapper.borrow_mut() = Some(client_rule_mapper);
|
||||||
|
|
@ -1812,6 +1854,9 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
|
||||||
.seat
|
.seat
|
||||||
.unstable_set_mouse_follows_focus(mouse_follows_focus);
|
.unstable_set_mouse_follows_focus(mouse_follows_focus);
|
||||||
}
|
}
|
||||||
|
if let Some(v) = config.autotile {
|
||||||
|
set_autotile(v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_command(exec: &Exec) -> Command {
|
fn create_command(exec: &Exec) -> Command {
|
||||||
|
|
@ -1822,6 +1867,9 @@ fn create_command(exec: &Exec) -> Command {
|
||||||
for (k, v) in &exec.envs {
|
for (k, v) in &exec.envs {
|
||||||
command.env(k, v);
|
command.env(k, v);
|
||||||
}
|
}
|
||||||
|
if let Some(tag) = &exec.tag {
|
||||||
|
command.tag(tag);
|
||||||
|
}
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,8 @@ impl Rule for ClientRule {
|
||||||
value_ref!(CommRegex, comm_regex);
|
value_ref!(CommRegex, comm_regex);
|
||||||
value_ref!(Exe, exe);
|
value_ref!(Exe, exe);
|
||||||
value_ref!(ExeRegex, exe_regex);
|
value_ref!(ExeRegex, exe_regex);
|
||||||
|
value_ref!(Tag, tag);
|
||||||
|
value_ref!(TagRegex, tag_regex);
|
||||||
value!(Uid, uid);
|
value!(Uid, uid);
|
||||||
value!(Pid, pid);
|
value!(Pid, pid);
|
||||||
bool!(Sandboxed, sandboxed);
|
bool!(Sandboxed, sandboxed);
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,54 @@
|
||||||
"name"
|
"name"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Sends the currently focused window to a scratchpad and hides it.\n\nA scratchpad can hold any number of windows. If `name` is omitted, the\ndefault scratchpad is used.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-shift-minus = { type = \"send-to-scratchpad\", name = \"terminal\" }\n ```\n",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "send-to-scratchpad"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the scratchpad."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Toggles a scratchpad.\n\nIf the scratchpad has a visible window, that window is hidden. Otherwise, the\nmost recently hidden window in the scratchpad is shown on the current workspace.\nOnly one window of a scratchpad is shown at a time, and scratchpad windows are\nalways shown floating. If `name` is omitted, the default scratchpad is used.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-minus = { type = \"toggle-scratchpad\", name = \"terminal\" }\n ```\n",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "toggle-scratchpad"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the scratchpad."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Cycles through the windows of a scratchpad, one at a time.\n\nWith no window shown, the first window is brought up. Each further invocation\nhides the current window and shows the next; after the last window the\nscratchpad is hidden again. Scratchpad windows are always shown floating.\nIf `name` is omitted, the default scratchpad is used.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-minus = { type = \"cycle-scratchpad\", name = \"terminal\" }\n ```\n",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "cycle-scratchpad"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the scratchpad."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Moves a workspace to a different output.\n\n- Example 1: Move a specific workspace to a named output\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"move-to-output\", workspace = \"1\", output.name = \"right\" }\n ```\n\n- Example 2: Move the current workspace to a named output\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"move-to-output\", output.name = \"right\" }\n ```\n\n- Example 3: Move the current workspace to the output on the right (directional)\n\n ```toml\n [shortcuts]\n \"logo+ctrl+shift+Right\" = { type = \"move-to-output\", direction = \"right\" }\n \"logo+ctrl+shift+Left\" = { type = \"move-to-output\", direction = \"left\" }\n \"logo+ctrl+shift+Up\" = { type = \"move-to-output\", direction = \"up\" }\n \"logo+ctrl+shift+Down\" = { type = \"move-to-output\", direction = \"down\" }\n ```\n",
|
"description": "Moves a workspace to a different output.\n\n- Example 1: Move a specific workspace to a named output\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"move-to-output\", workspace = \"1\", output.name = \"right\" }\n ```\n\n- Example 2: Move the current workspace to a named output\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"move-to-output\", output.name = \"right\" }\n ```\n\n- Example 3: Move the current workspace to the output on the right (directional)\n\n ```toml\n [shortcuts]\n \"logo+ctrl+shift+Right\" = { type = \"move-to-output\", direction = \"right\" }\n \"logo+ctrl+shift+Left\" = { type = \"move-to-output\", direction = \"left\" }\n \"logo+ctrl+shift+Up\" = { type = \"move-to-output\", direction = \"up\" }\n \"logo+ctrl+shift+Down\" = { type = \"move-to-output\", direction = \"down\" }\n ```\n",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
@ -841,6 +889,14 @@
|
||||||
"exe-regex": {
|
"exe-regex": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Matches the `/proc/pid/exe` of the client with a regular expression."
|
"description": "Matches the `/proc/pid/exe` of the client with a regular expression."
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Matches the tag of the client verbatim."
|
||||||
|
},
|
||||||
|
"tag-regex": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Matches the tag of the client with a regular expression."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": []
|
"required": []
|
||||||
|
|
@ -1157,6 +1213,10 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Configures whether middle-click pasting is enabled.\n\nChanging this has no effect on running applications.\n\nThe default is `true`.\n"
|
"description": "Configures whether middle-click pasting is enabled.\n\nChanging this has no effect on running applications.\n\nThe default is `true`.\n"
|
||||||
},
|
},
|
||||||
|
"autotile": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Configures whether autotiling is enabled by default.\n\nWhen enabled, newly mapped tiled windows alternate their split\norientation automatically. This can also be toggled at runtime via the\n`enable-autotile`, `disable-autotile`, and `toggle-autotile` actions.\n\nThe default is `false`.\n"
|
||||||
|
},
|
||||||
"modes": {
|
"modes": {
|
||||||
"description": "Configures the input modes.\n\nModes can be used to define shortcuts that are only active when the mode is\nactive.\n\n- Example\n\n ```toml\n [modes.\"navigation\".shortcuts]\n w = \"focus-up\"\n a = \"focus-left\"\n s = \"focus-down\"\n d = \"focus-right\"\n r = \"focus-above\"\n f = \"focus-below\"\n q = \"focus-prev\"\n e = \"focus-next\"\n ```\n\nModes can be activated with the `push-mode` and `latch-mode` actions.\n",
|
"description": "Configures the input modes.\n\nModes can be used to define shortcuts that are only active when the mode is\nactive.\n\n- Example\n\n ```toml\n [modes.\"navigation\".shortcuts]\n w = \"focus-up\"\n a = \"focus-left\"\n s = \"focus-down\"\n d = \"focus-right\"\n r = \"focus-above\"\n f = \"focus-below\"\n q = \"focus-prev\"\n e = \"focus-next\"\n ```\n\nModes can be activated with the `push-mode` and `latch-mode` actions.\n",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
@ -1184,6 +1244,14 @@
|
||||||
"egui": {
|
"egui": {
|
||||||
"description": "Sets the egui settings of the compositor.\n",
|
"description": "Sets the egui settings of the compositor.\n",
|
||||||
"$ref": "#/$defs/Egui"
|
"$ref": "#/$defs/Egui"
|
||||||
|
},
|
||||||
|
"scratchpads": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "An array of pre-configured scratchpads.\n\nEach entry launches a program when the graphics are first initialized and\nimmediately parks its window in the named scratchpad. The window is captured\nvia a unique tag attached to the spawned process, so other windows of the\nsame application are never affected.\n\nUse a `toggle-scratchpad` or `cycle-scratchpad` action to bring the windows\nup; they are always shown floating.\n\n- Example:\n\n ```toml\n [[scratchpads]]\n name = \"term\"\n exec = \"foot\"\n\n [[scratchpads]]\n name = \"notes\"\n exec = [\"obsidian\"]\n ```\n",
|
||||||
|
"items": {
|
||||||
|
"description": "",
|
||||||
|
"$ref": "#/$defs/Scratchpad"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": []
|
"required": []
|
||||||
|
|
@ -1413,6 +1481,10 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": ""
|
"description": ""
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Specifies a tag to apply to all spawned wayland connections.\n"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": []
|
"required": []
|
||||||
|
|
@ -1990,6 +2062,23 @@
|
||||||
},
|
},
|
||||||
"required": []
|
"required": []
|
||||||
},
|
},
|
||||||
|
"Scratchpad": {
|
||||||
|
"description": "A pre-configured scratchpad whose program is launched at startup and parked\nin the scratchpad.\n\n- Example:\n\n ```toml\n [[scratchpads]]\n name = \"term\"\n exec = \"foot\"\n ```\n",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the scratchpad that the spawned window is parked in."
|
||||||
|
},
|
||||||
|
"exec": {
|
||||||
|
"description": "The program to launch when the graphics are first initialized.\n\nIf omitted, no program is launched and the scratchpad is only created on\ndemand by `send-to-scratchpad`.\n",
|
||||||
|
"$ref": "#/$defs/Exec"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
},
|
||||||
"SimpleActionName": {
|
"SimpleActionName": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The name of a `simple` Action.\n\nWhen used inside a window rule, the following actions apply to the matched window\ninstead fo the focused window:\n\n- `move-left`\n- `move-down`\n- `move-up`\n- `move-right`\n- `toggle-fullscreen`\n- `enter-fullscreen`\n- `exit-fullscreen`\n- `close`\n- `toggle-floating`\n- `float`\n- `tile`\n- `toggle-float-pinned`\n- `pin-float`\n- `unpin-float`\n\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-q = \"quit\"\n ```\n",
|
"description": "The name of a `simple` Action.\n\nWhen used inside a window rule, the following actions apply to the matched window\ninstead fo the focused window:\n\n- `move-left`\n- `move-down`\n- `move-up`\n- `move-right`\n- `toggle-fullscreen`\n- `enter-fullscreen`\n- `exit-fullscreen`\n- `close`\n- `toggle-floating`\n- `float`\n- `tile`\n- `toggle-float-pinned`\n- `pin-float`\n- `unpin-float`\n\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-q = \"quit\"\n ```\n",
|
||||||
|
|
@ -2008,9 +2097,15 @@
|
||||||
"make-group-tab",
|
"make-group-tab",
|
||||||
"change-group-opposite",
|
"change-group-opposite",
|
||||||
"toggle-tab",
|
"toggle-tab",
|
||||||
|
"enable-autotile",
|
||||||
|
"disable-autotile",
|
||||||
|
"toggle-autotile",
|
||||||
"toggle-fullscreen",
|
"toggle-fullscreen",
|
||||||
"enter-fullscreen",
|
"enter-fullscreen",
|
||||||
"exit-fullscreen",
|
"exit-fullscreen",
|
||||||
|
"send-to-scratchpad",
|
||||||
|
"toggle-scratchpad",
|
||||||
|
"cycle-scratchpad",
|
||||||
"focus-parent",
|
"focus-parent",
|
||||||
"close",
|
"close",
|
||||||
"disable-pointer-constraint",
|
"disable-pointer-constraint",
|
||||||
|
|
|
||||||
|
|
@ -286,6 +286,76 @@ This table is a tagged union. The variant is determined by the `type` field. It
|
||||||
|
|
||||||
The value of this field should be a string.
|
The value of this field should be a string.
|
||||||
|
|
||||||
|
- `send-to-scratchpad`:
|
||||||
|
|
||||||
|
Sends the currently focused window to a scratchpad and hides it.
|
||||||
|
|
||||||
|
A scratchpad can hold any number of windows. If `name` is omitted, the
|
||||||
|
default scratchpad is used.
|
||||||
|
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[shortcuts]
|
||||||
|
alt-shift-minus = { type = "send-to-scratchpad", name = "terminal" }
|
||||||
|
```
|
||||||
|
|
||||||
|
The table has the following fields:
|
||||||
|
|
||||||
|
- `name` (optional):
|
||||||
|
|
||||||
|
The name of the scratchpad.
|
||||||
|
|
||||||
|
The value of this field should be a string.
|
||||||
|
|
||||||
|
- `toggle-scratchpad`:
|
||||||
|
|
||||||
|
Toggles a scratchpad.
|
||||||
|
|
||||||
|
If the scratchpad has a visible window, that window is hidden. Otherwise, the
|
||||||
|
most recently hidden window in the scratchpad is shown on the current workspace.
|
||||||
|
Only one window of a scratchpad is shown at a time, and scratchpad windows are
|
||||||
|
always shown floating. If `name` is omitted, the default scratchpad is used.
|
||||||
|
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[shortcuts]
|
||||||
|
alt-minus = { type = "toggle-scratchpad", name = "terminal" }
|
||||||
|
```
|
||||||
|
|
||||||
|
The table has the following fields:
|
||||||
|
|
||||||
|
- `name` (optional):
|
||||||
|
|
||||||
|
The name of the scratchpad.
|
||||||
|
|
||||||
|
The value of this field should be a string.
|
||||||
|
|
||||||
|
- `cycle-scratchpad`:
|
||||||
|
|
||||||
|
Cycles through the windows of a scratchpad, one at a time.
|
||||||
|
|
||||||
|
With no window shown, the first window is brought up. Each further invocation
|
||||||
|
hides the current window and shows the next; after the last window the
|
||||||
|
scratchpad is hidden again. Scratchpad windows are always shown floating.
|
||||||
|
If `name` is omitted, the default scratchpad is used.
|
||||||
|
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[shortcuts]
|
||||||
|
alt-minus = { type = "cycle-scratchpad", name = "terminal" }
|
||||||
|
```
|
||||||
|
|
||||||
|
The table has the following fields:
|
||||||
|
|
||||||
|
- `name` (optional):
|
||||||
|
|
||||||
|
The name of the scratchpad.
|
||||||
|
|
||||||
|
The value of this field should be a string.
|
||||||
|
|
||||||
- `move-to-output`:
|
- `move-to-output`:
|
||||||
|
|
||||||
Moves a workspace to a different output.
|
Moves a workspace to a different output.
|
||||||
|
|
@ -1409,6 +1479,18 @@ The table has the following fields:
|
||||||
|
|
||||||
The value of this field should be a string.
|
The value of this field should be a string.
|
||||||
|
|
||||||
|
- `tag` (optional):
|
||||||
|
|
||||||
|
Matches the tag of the client verbatim.
|
||||||
|
|
||||||
|
The value of this field should be a string.
|
||||||
|
|
||||||
|
- `tag-regex` (optional):
|
||||||
|
|
||||||
|
Matches the tag of the client with a regular expression.
|
||||||
|
|
||||||
|
The value of this field should be a string.
|
||||||
|
|
||||||
|
|
||||||
<a name="types-ClientMatchExactly"></a>
|
<a name="types-ClientMatchExactly"></a>
|
||||||
### `ClientMatchExactly`
|
### `ClientMatchExactly`
|
||||||
|
|
@ -2354,6 +2436,18 @@ The table has the following fields:
|
||||||
|
|
||||||
The value of this field should be a boolean.
|
The value of this field should be a boolean.
|
||||||
|
|
||||||
|
- `autotile` (optional):
|
||||||
|
|
||||||
|
Configures whether autotiling is enabled by default.
|
||||||
|
|
||||||
|
When enabled, newly mapped tiled windows alternate their split
|
||||||
|
orientation automatically. This can also be toggled at runtime via the
|
||||||
|
`enable-autotile`, `disable-autotile`, and `toggle-autotile` actions.
|
||||||
|
|
||||||
|
The default is `false`.
|
||||||
|
|
||||||
|
The value of this field should be a boolean.
|
||||||
|
|
||||||
- `modes` (optional):
|
- `modes` (optional):
|
||||||
|
|
||||||
Configures the input modes.
|
Configures the input modes.
|
||||||
|
|
@ -2454,6 +2548,32 @@ The table has the following fields:
|
||||||
|
|
||||||
The value of this field should be a [Egui](#types-Egui).
|
The value of this field should be a [Egui](#types-Egui).
|
||||||
|
|
||||||
|
- `scratchpads` (optional):
|
||||||
|
|
||||||
|
An array of pre-configured scratchpads.
|
||||||
|
|
||||||
|
Each entry launches a program when the graphics are first initialized and
|
||||||
|
immediately parks its window in the named scratchpad. The window is captured
|
||||||
|
via a unique tag attached to the spawned process, so other windows of the
|
||||||
|
same application are never affected.
|
||||||
|
|
||||||
|
Use a `toggle-scratchpad` or `cycle-scratchpad` action to bring the windows
|
||||||
|
up; they are always shown floating.
|
||||||
|
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[scratchpads]]
|
||||||
|
name = "term"
|
||||||
|
exec = "foot"
|
||||||
|
|
||||||
|
[[scratchpads]]
|
||||||
|
name = "notes"
|
||||||
|
exec = ["obsidian"]
|
||||||
|
```
|
||||||
|
|
||||||
|
The value of this field should be an array of [Scratchpads](#types-Scratchpad).
|
||||||
|
|
||||||
|
|
||||||
<a name="types-Connector"></a>
|
<a name="types-Connector"></a>
|
||||||
### `Connector`
|
### `Connector`
|
||||||
|
|
@ -2933,6 +3053,12 @@ The table has the following fields:
|
||||||
|
|
||||||
The value of this field should be a table whose values are strings.
|
The value of this field should be a table whose values are strings.
|
||||||
|
|
||||||
|
- `tag` (optional):
|
||||||
|
|
||||||
|
Specifies a tag to apply to all spawned wayland connections.
|
||||||
|
|
||||||
|
The value of this field should be a string.
|
||||||
|
|
||||||
|
|
||||||
<a name="types-FallbackOutputMode"></a>
|
<a name="types-FallbackOutputMode"></a>
|
||||||
### `FallbackOutputMode`
|
### `FallbackOutputMode`
|
||||||
|
|
@ -4357,6 +4483,40 @@ The table has the following fields:
|
||||||
The value of this field should be a string.
|
The value of this field should be a string.
|
||||||
|
|
||||||
|
|
||||||
|
<a name="types-Scratchpad"></a>
|
||||||
|
### `Scratchpad`
|
||||||
|
|
||||||
|
A pre-configured scratchpad whose program is launched at startup and parked
|
||||||
|
in the scratchpad.
|
||||||
|
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[scratchpads]]
|
||||||
|
name = "term"
|
||||||
|
exec = "foot"
|
||||||
|
```
|
||||||
|
|
||||||
|
Values of this type should be tables.
|
||||||
|
|
||||||
|
The table has the following fields:
|
||||||
|
|
||||||
|
- `name` (required):
|
||||||
|
|
||||||
|
The name of the scratchpad that the spawned window is parked in.
|
||||||
|
|
||||||
|
The value of this field should be a string.
|
||||||
|
|
||||||
|
- `exec` (optional):
|
||||||
|
|
||||||
|
The program to launch when the graphics are first initialized.
|
||||||
|
|
||||||
|
If omitted, no program is launched and the scratchpad is only created on
|
||||||
|
demand by `send-to-scratchpad`.
|
||||||
|
|
||||||
|
The value of this field should be a [Exec](#types-Exec).
|
||||||
|
|
||||||
|
|
||||||
<a name="types-SimpleActionName"></a>
|
<a name="types-SimpleActionName"></a>
|
||||||
### `SimpleActionName`
|
### `SimpleActionName`
|
||||||
|
|
||||||
|
|
@ -4448,6 +4608,18 @@ The string should have one of the following values:
|
||||||
|
|
||||||
Toggles the current group between tabbed and split mode.
|
Toggles the current group between tabbed and split mode.
|
||||||
|
|
||||||
|
- `enable-autotile`:
|
||||||
|
|
||||||
|
Enables alternating split orientation for newly tiled windows.
|
||||||
|
|
||||||
|
- `disable-autotile`:
|
||||||
|
|
||||||
|
Disables alternating split orientation for newly tiled windows.
|
||||||
|
|
||||||
|
- `toggle-autotile`:
|
||||||
|
|
||||||
|
Toggles alternating split orientation for newly tiled windows.
|
||||||
|
|
||||||
- `toggle-fullscreen`:
|
- `toggle-fullscreen`:
|
||||||
|
|
||||||
Toggle the currently focused window between fullscreen and windowed.
|
Toggle the currently focused window between fullscreen and windowed.
|
||||||
|
|
@ -4460,6 +4632,18 @@ The string should have one of the following values:
|
||||||
|
|
||||||
Makes the currently focused window windowed.
|
Makes the currently focused window windowed.
|
||||||
|
|
||||||
|
- `send-to-scratchpad`:
|
||||||
|
|
||||||
|
Sends the currently focused window to the default scratchpad.
|
||||||
|
|
||||||
|
- `toggle-scratchpad`:
|
||||||
|
|
||||||
|
Toggles the default scratchpad.
|
||||||
|
|
||||||
|
- `cycle-scratchpad`:
|
||||||
|
|
||||||
|
Cycles through the windows of the default scratchpad.
|
||||||
|
|
||||||
- `focus-parent`:
|
- `focus-parent`:
|
||||||
|
|
||||||
Focus the parent of the currently focused window.
|
Focus the parent of the currently focused window.
|
||||||
|
|
|
||||||
|
|
@ -345,6 +345,64 @@ Action:
|
||||||
description: The name of the workspace.
|
description: The name of the workspace.
|
||||||
required: true
|
required: true
|
||||||
kind: string
|
kind: string
|
||||||
|
send-to-scratchpad:
|
||||||
|
description: |
|
||||||
|
Sends the currently focused window to a scratchpad and hides it.
|
||||||
|
|
||||||
|
A scratchpad can hold any number of windows. If `name` is omitted, the
|
||||||
|
default scratchpad is used.
|
||||||
|
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[shortcuts]
|
||||||
|
alt-shift-minus = { type = "send-to-scratchpad", name = "terminal" }
|
||||||
|
```
|
||||||
|
fields:
|
||||||
|
name:
|
||||||
|
description: The name of the scratchpad.
|
||||||
|
required: false
|
||||||
|
kind: string
|
||||||
|
toggle-scratchpad:
|
||||||
|
description: |
|
||||||
|
Toggles a scratchpad.
|
||||||
|
|
||||||
|
If the scratchpad has a visible window, that window is hidden. Otherwise, the
|
||||||
|
most recently hidden window in the scratchpad is shown on the current workspace.
|
||||||
|
Only one window of a scratchpad is shown at a time, and scratchpad windows are
|
||||||
|
always shown floating. If `name` is omitted, the default scratchpad is used.
|
||||||
|
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[shortcuts]
|
||||||
|
alt-minus = { type = "toggle-scratchpad", name = "terminal" }
|
||||||
|
```
|
||||||
|
fields:
|
||||||
|
name:
|
||||||
|
description: The name of the scratchpad.
|
||||||
|
required: false
|
||||||
|
kind: string
|
||||||
|
cycle-scratchpad:
|
||||||
|
description: |
|
||||||
|
Cycles through the windows of a scratchpad, one at a time.
|
||||||
|
|
||||||
|
With no window shown, the first window is brought up. Each further invocation
|
||||||
|
hides the current window and shows the next; after the last window the
|
||||||
|
scratchpad is hidden again. Scratchpad windows are always shown floating.
|
||||||
|
If `name` is omitted, the default scratchpad is used.
|
||||||
|
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[shortcuts]
|
||||||
|
alt-minus = { type = "cycle-scratchpad", name = "terminal" }
|
||||||
|
```
|
||||||
|
fields:
|
||||||
|
name:
|
||||||
|
description: The name of the scratchpad.
|
||||||
|
required: false
|
||||||
|
kind: string
|
||||||
move-to-output:
|
move-to-output:
|
||||||
description: |
|
description: |
|
||||||
Moves a workspace to a different output.
|
Moves a workspace to a different output.
|
||||||
|
|
@ -978,6 +1036,11 @@ Exec:
|
||||||
values:
|
values:
|
||||||
kind: string
|
kind: string
|
||||||
description: The environment variables to pass to the executable.
|
description: The environment variables to pass to the executable.
|
||||||
|
tag:
|
||||||
|
kind: string
|
||||||
|
required: false
|
||||||
|
description: |
|
||||||
|
Specifies a tag to apply to all spawned wayland connections.
|
||||||
|
|
||||||
|
|
||||||
SimpleActionName:
|
SimpleActionName:
|
||||||
|
|
@ -1039,12 +1102,24 @@ SimpleActionName:
|
||||||
description: Toggles the current group's direction.
|
description: Toggles the current group's direction.
|
||||||
- value: toggle-tab
|
- value: toggle-tab
|
||||||
description: Toggles the current group between tabbed and split mode.
|
description: Toggles the current group between tabbed and split mode.
|
||||||
|
- value: enable-autotile
|
||||||
|
description: Enables alternating split orientation for newly tiled windows.
|
||||||
|
- value: disable-autotile
|
||||||
|
description: Disables alternating split orientation for newly tiled windows.
|
||||||
|
- value: toggle-autotile
|
||||||
|
description: Toggles alternating split orientation for newly tiled windows.
|
||||||
- value: toggle-fullscreen
|
- value: toggle-fullscreen
|
||||||
description: Toggle the currently focused window between fullscreen and windowed.
|
description: Toggle the currently focused window between fullscreen and windowed.
|
||||||
- value: enter-fullscreen
|
- value: enter-fullscreen
|
||||||
description: Makes the currently focused window fullscreen.
|
description: Makes the currently focused window fullscreen.
|
||||||
- value: exit-fullscreen
|
- value: exit-fullscreen
|
||||||
description: Makes the currently focused window windowed.
|
description: Makes the currently focused window windowed.
|
||||||
|
- value: send-to-scratchpad
|
||||||
|
description: Sends the currently focused window to the default scratchpad.
|
||||||
|
- value: toggle-scratchpad
|
||||||
|
description: Toggles the default scratchpad.
|
||||||
|
- value: cycle-scratchpad
|
||||||
|
description: Cycles through the windows of the default scratchpad.
|
||||||
- value: focus-parent
|
- value: focus-parent
|
||||||
description: Focus the parent of the currently focused window.
|
description: Focus the parent of the currently focused window.
|
||||||
- value: close
|
- value: close
|
||||||
|
|
@ -3102,10 +3177,21 @@ Config:
|
||||||
required: false
|
required: false
|
||||||
description: |
|
description: |
|
||||||
Configures whether middle-click pasting is enabled.
|
Configures whether middle-click pasting is enabled.
|
||||||
|
|
||||||
Changing this has no effect on running applications.
|
Changing this has no effect on running applications.
|
||||||
|
|
||||||
The default is `true`.
|
The default is `true`.
|
||||||
|
autotile:
|
||||||
|
kind: boolean
|
||||||
|
required: false
|
||||||
|
description: |
|
||||||
|
Configures whether autotiling is enabled by default.
|
||||||
|
|
||||||
|
When enabled, newly mapped tiled windows alternate their split
|
||||||
|
orientation automatically. This can also be toggled at runtime via the
|
||||||
|
`enable-autotile`, `disable-autotile`, and `toggle-autotile` actions.
|
||||||
|
|
||||||
|
The default is `false`.
|
||||||
modes:
|
modes:
|
||||||
kind: map
|
kind: map
|
||||||
values:
|
values:
|
||||||
|
|
@ -3202,6 +3288,61 @@ Config:
|
||||||
required: false
|
required: false
|
||||||
description: |
|
description: |
|
||||||
Sets the egui settings of the compositor.
|
Sets the egui settings of the compositor.
|
||||||
|
scratchpads:
|
||||||
|
kind: array
|
||||||
|
items:
|
||||||
|
ref: Scratchpad
|
||||||
|
required: false
|
||||||
|
description: |
|
||||||
|
An array of pre-configured scratchpads.
|
||||||
|
|
||||||
|
Each entry launches a program when the graphics are first initialized and
|
||||||
|
immediately parks its window in the named scratchpad. The window is captured
|
||||||
|
via a unique tag attached to the spawned process, so other windows of the
|
||||||
|
same application are never affected.
|
||||||
|
|
||||||
|
Use a `toggle-scratchpad` or `cycle-scratchpad` action to bring the windows
|
||||||
|
up; they are always shown floating.
|
||||||
|
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[scratchpads]]
|
||||||
|
name = "term"
|
||||||
|
exec = "foot"
|
||||||
|
|
||||||
|
[[scratchpads]]
|
||||||
|
name = "notes"
|
||||||
|
exec = ["obsidian"]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Scratchpad:
|
||||||
|
kind: table
|
||||||
|
description: |
|
||||||
|
A pre-configured scratchpad whose program is launched at startup and parked
|
||||||
|
in the scratchpad.
|
||||||
|
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[scratchpads]]
|
||||||
|
name = "term"
|
||||||
|
exec = "foot"
|
||||||
|
```
|
||||||
|
fields:
|
||||||
|
name:
|
||||||
|
kind: string
|
||||||
|
required: true
|
||||||
|
description: The name of the scratchpad that the spawned window is parked in.
|
||||||
|
exec:
|
||||||
|
ref: Exec
|
||||||
|
required: false
|
||||||
|
description: |
|
||||||
|
The program to launch when the graphics are first initialized.
|
||||||
|
|
||||||
|
If omitted, no program is launched and the scratchpad is only created on
|
||||||
|
demand by `send-to-scratchpad`.
|
||||||
|
|
||||||
|
|
||||||
Idle:
|
Idle:
|
||||||
|
|
@ -4110,6 +4251,14 @@ ClientMatch:
|
||||||
kind: string
|
kind: string
|
||||||
required: false
|
required: false
|
||||||
description: Matches the `/proc/pid/exe` of the client with a regular expression.
|
description: Matches the `/proc/pid/exe` of the client with a regular expression.
|
||||||
|
tag:
|
||||||
|
kind: string
|
||||||
|
required: false
|
||||||
|
description: Matches the tag of the client verbatim.
|
||||||
|
tag-regex:
|
||||||
|
kind: string
|
||||||
|
required: false
|
||||||
|
description: Matches the tag of the client with a regular expression.
|
||||||
|
|
||||||
|
|
||||||
ClientMatchExactly:
|
ClientMatchExactly:
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ use {
|
||||||
std::{io, os::unix::ffi::OsStrExt, path::PathBuf},
|
std::{io, os::unix::ffi::OsStrExt, path::PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[path = "../../build/wire/parser.rs"]
|
#[path = "../../../build/wire/parser.rs"]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
mod parser;
|
mod parser;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ pub struct ClientMetadata {
|
||||||
pub sandbox_engine: Option<String>,
|
pub sandbox_engine: Option<String>,
|
||||||
pub app_id: Option<String>,
|
pub app_id: Option<String>,
|
||||||
pub instance_id: Option<String>,
|
pub instance_id: Option<String>,
|
||||||
|
pub tag: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clients {
|
impl Clients {
|
||||||
|
|
|
||||||
|
|
@ -286,6 +286,7 @@ fn start_compositor2(
|
||||||
display: Default::default(),
|
display: Default::default(),
|
||||||
},
|
},
|
||||||
acceptor: Default::default(),
|
acceptor: Default::default(),
|
||||||
|
tagged_acceptors: Default::default(),
|
||||||
serial: Default::default(),
|
serial: Default::default(),
|
||||||
idle_inhibitor_ids: Default::default(),
|
idle_inhibitor_ids: Default::default(),
|
||||||
run_toplevel,
|
run_toplevel,
|
||||||
|
|
@ -385,6 +386,7 @@ fn start_compositor2(
|
||||||
bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)),
|
bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)),
|
||||||
virtual_outputs: Default::default(),
|
virtual_outputs: Default::default(),
|
||||||
clean_logs_older_than: Default::default(),
|
clean_logs_older_than: Default::default(),
|
||||||
|
scratchpads: Default::default(),
|
||||||
});
|
});
|
||||||
state.tracker.register(ClientId::from_raw(0));
|
state.tracker.register(ClientId::from_raw(0));
|
||||||
create_dummy_output(&state);
|
create_dummy_output(&state);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use {
|
||||||
},
|
},
|
||||||
client::{Client, ClientId},
|
client::{Client, ClientId},
|
||||||
cmm::cmm_eotf::Eotf,
|
cmm::cmm_eotf::Eotf,
|
||||||
compositor::MAX_EXTENTS,
|
compositor::{MAX_EXTENTS, WAYLAND_DISPLAY},
|
||||||
criteria::{
|
criteria::{
|
||||||
CritLiteralOrRegex, CritMgrExt, CritTarget, CritUpstreamNode,
|
CritLiteralOrRegex, CritMgrExt, CritTarget, CritUpstreamNode,
|
||||||
clm::ClmLeafMatcher,
|
clm::ClmLeafMatcher,
|
||||||
|
|
@ -25,6 +25,7 @@ use {
|
||||||
output_schedule::map_cursor_hz,
|
output_schedule::map_cursor_hz,
|
||||||
scale::Scale,
|
scale::Scale,
|
||||||
state::{ConnectorData, DeviceHandlerData, DrmDevData, OutputData, State},
|
state::{ConnectorData, DeviceHandlerData, DrmDevData, OutputData, State},
|
||||||
|
tagged_acceptor::TaggedAcceptorError,
|
||||||
theme::{ThemeColor, ThemeSized},
|
theme::{ThemeColor, ThemeSized},
|
||||||
tree::{
|
tree::{
|
||||||
ContainerSplit, OutputNode, TearingMode, TileState, ToplevelData, ToplevelIdentifier,
|
ContainerSplit, OutputNode, TearingMode, TileState, ToplevelData, ToplevelIdentifier,
|
||||||
|
|
@ -406,6 +407,8 @@ enum CphError {
|
||||||
UnknownFallbackOutputMode(FallbackOutputMode),
|
UnknownFallbackOutputMode(FallbackOutputMode),
|
||||||
#[error("Unknown tile state {0:?}")]
|
#[error("Unknown tile state {0:?}")]
|
||||||
UnknownTileState(ConfigTileState),
|
UnknownTileState(ConfigTileState),
|
||||||
|
#[error("Could not create tagged acceptor")]
|
||||||
|
CreateTaggedAcceptor(#[source] TaggedAcceptorError),
|
||||||
}
|
}
|
||||||
|
|
||||||
trait WithRequestName {
|
trait WithRequestName {
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ impl ConfigProxyHandler {
|
||||||
ClientMessage::GetSeats => self.handle_get_seats(),
|
ClientMessage::GetSeats => self.handle_get_seats(),
|
||||||
ClientMessage::RemoveSeat { .. } => {}
|
ClientMessage::RemoveSeat { .. } => {}
|
||||||
ClientMessage::Run { prog, args, env } => {
|
ClientMessage::Run { prog, args, env } => {
|
||||||
self.handle_run(prog, args, env, vec![]).wrn("run")?
|
self.handle_run(prog, args, env, vec![], None).wrn("run")?
|
||||||
}
|
}
|
||||||
ClientMessage::GrabKb { kb, grab } => self.handle_grab(kb, grab).wrn("grab")?,
|
ClientMessage::GrabKb { kb, grab } => self.handle_grab(kb, grab).wrn("grab")?,
|
||||||
ClientMessage::SetColor { colorable, color } => {
|
ClientMessage::SetColor { colorable, color } => {
|
||||||
|
|
@ -111,6 +111,15 @@ impl ConfigProxyHandler {
|
||||||
ClientMessage::SetSeatWorkspace { seat, workspace } => self
|
ClientMessage::SetSeatWorkspace { seat, workspace } => self
|
||||||
.handle_set_seat_workspace(seat, workspace)
|
.handle_set_seat_workspace(seat, workspace)
|
||||||
.wrn("set_seat_workspace")?,
|
.wrn("set_seat_workspace")?,
|
||||||
|
ClientMessage::SeatSendToScratchpad { seat, name } => self
|
||||||
|
.handle_seat_send_to_scratchpad(seat, name)
|
||||||
|
.wrn("seat_send_to_scratchpad")?,
|
||||||
|
ClientMessage::SeatToggleScratchpad { seat, name } => self
|
||||||
|
.handle_seat_toggle_scratchpad(seat, name)
|
||||||
|
.wrn("seat_toggle_scratchpad")?,
|
||||||
|
ClientMessage::SeatCycleScratchpad { seat, name } => self
|
||||||
|
.handle_seat_cycle_scratchpad(seat, name)
|
||||||
|
.wrn("seat_cycle_scratchpad")?,
|
||||||
ClientMessage::GetConnector { ty, idx } => {
|
ClientMessage::GetConnector { ty, idx } => {
|
||||||
self.handle_get_connector(ty, idx).wrn("get_connector")?
|
self.handle_get_connector(ty, idx).wrn("get_connector")?
|
||||||
}
|
}
|
||||||
|
|
@ -268,7 +277,14 @@ impl ConfigProxyHandler {
|
||||||
args,
|
args,
|
||||||
env,
|
env,
|
||||||
fds,
|
fds,
|
||||||
} => self.handle_run(prog, args, env, fds).wrn("run")?,
|
} => self.handle_run(prog, args, env, fds, None).wrn("run")?,
|
||||||
|
ClientMessage::Run3 {
|
||||||
|
prog,
|
||||||
|
args,
|
||||||
|
env,
|
||||||
|
fds,
|
||||||
|
tag,
|
||||||
|
} => self.handle_run(prog, args, env, fds, tag).wrn("run")?,
|
||||||
ClientMessage::DisableDefaultSeat => self.state.create_default_seat.set(false),
|
ClientMessage::DisableDefaultSeat => self.state.create_default_seat.set(false),
|
||||||
ClientMessage::DestroyKeymap { keymap } => self.handle_destroy_keymap(keymap),
|
ClientMessage::DestroyKeymap { keymap } => self.handle_destroy_keymap(keymap),
|
||||||
ClientMessage::GetConnectorName { connector } => self
|
ClientMessage::GetConnectorName { connector } => self
|
||||||
|
|
@ -500,6 +516,9 @@ impl ConfigProxyHandler {
|
||||||
ClientMessage::SetWindowWorkspace { window, workspace } => self
|
ClientMessage::SetWindowWorkspace { window, workspace } => self
|
||||||
.handle_set_window_workspace(window, workspace)
|
.handle_set_window_workspace(window, workspace)
|
||||||
.wrn("set_window_workspace")?,
|
.wrn("set_window_workspace")?,
|
||||||
|
ClientMessage::WindowSendToScratchpad { window, name } => self
|
||||||
|
.handle_window_send_to_scratchpad(window, name)
|
||||||
|
.wrn("window_send_to_scratchpad")?,
|
||||||
ClientMessage::SetWindowFullscreen { window, fullscreen } => self
|
ClientMessage::SetWindowFullscreen { window, fullscreen } => self
|
||||||
.handle_set_window_fullscreen(window, fullscreen)
|
.handle_set_window_fullscreen(window, fullscreen)
|
||||||
.wrn("set_window_fullscreen")?,
|
.wrn("set_window_fullscreen")?,
|
||||||
|
|
@ -701,6 +720,11 @@ impl ConfigProxyHandler {
|
||||||
ClientMessage::SetAutotile { enabled } => {
|
ClientMessage::SetAutotile { enabled } => {
|
||||||
self.state.theme.autotile_enabled.set(enabled);
|
self.state.theme.autotile_enabled.set(enabled);
|
||||||
}
|
}
|
||||||
|
ClientMessage::GetAutotile => {
|
||||||
|
self.respond(Response::GetAutotile {
|
||||||
|
enabled: self.state.theme.autotile_enabled.get(),
|
||||||
|
});
|
||||||
|
}
|
||||||
ClientMessage::SeatToggleExpand { .. } => {
|
ClientMessage::SeatToggleExpand { .. } => {
|
||||||
// Removed feature; kept for binary protocol compatibility.
|
// Removed feature; kept for binary protocol compatibility.
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ impl ConfigProxyHandler {
|
||||||
}
|
}
|
||||||
ClientCriterionStringField::Comm => mgr.comm(needle),
|
ClientCriterionStringField::Comm => mgr.comm(needle),
|
||||||
ClientCriterionStringField::Exe => mgr.exe(needle),
|
ClientCriterionStringField::Exe => mgr.exe(needle),
|
||||||
|
ClientCriterionStringField::Tag => mgr.tag(needle),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ClientCriterionPayload::Sandboxed => mgr.sandboxed(),
|
ClientCriterionPayload::Sandboxed => mgr.sandboxed(),
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,18 @@ impl ConfigProxyHandler {
|
||||||
&self,
|
&self,
|
||||||
prog: &str,
|
prog: &str,
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
env: Vec<(String, String)>,
|
mut env: Vec<(String, String)>,
|
||||||
fds: Vec<(i32, i32)>,
|
fds: Vec<(i32, i32)>,
|
||||||
|
tag: Option<&str>,
|
||||||
) -> Result<(), CphError> {
|
) -> Result<(), CphError> {
|
||||||
|
if let Some(tag) = tag {
|
||||||
|
let display = self
|
||||||
|
.state
|
||||||
|
.tagged_acceptors
|
||||||
|
.get(&self.state, tag)
|
||||||
|
.map_err(CphError::CreateTaggedAcceptor)?;
|
||||||
|
env.push((WAYLAND_DISPLAY.to_string(), display.to_string()));
|
||||||
|
}
|
||||||
let fds: Vec<_> = fds
|
let fds: Vec<_> = fds
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(a, b)| (a, Rc::new(OwnedFd::new(b))))
|
.map(|(a, b)| (a, Rc::new(OwnedFd::new(b))))
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,44 @@ impl ConfigProxyHandler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn handle_seat_send_to_scratchpad(
|
||||||
|
&self,
|
||||||
|
seat: Seat,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<(), CphError> {
|
||||||
|
self.state.with_linear_layout_animations(|| {
|
||||||
|
let seat = self.get_seat(seat)?;
|
||||||
|
if let Some(toplevel) = seat.get_keyboard_node().node_toplevel() {
|
||||||
|
self.state.send_to_scratchpad(name, toplevel);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn handle_seat_toggle_scratchpad(
|
||||||
|
&self,
|
||||||
|
seat: Seat,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<(), CphError> {
|
||||||
|
self.state.with_linear_layout_animations(|| {
|
||||||
|
let seat = self.get_seat(seat)?;
|
||||||
|
self.state.toggle_scratchpad(&seat, name);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn handle_seat_cycle_scratchpad(
|
||||||
|
&self,
|
||||||
|
seat: Seat,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<(), CphError> {
|
||||||
|
self.state.with_linear_layout_animations(|| {
|
||||||
|
let seat = self.get_seat(seat)?;
|
||||||
|
self.state.cycle_scratchpad(&seat, name);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn handle_get_repeat_rate(&self, seat: Seat) -> Result<(), CphError> {
|
pub(super) fn handle_get_repeat_rate(&self, seat: Seat) -> Result<(), CphError> {
|
||||||
let seat = self.get_seat(seat)?;
|
let seat = self.get_seat(seat)?;
|
||||||
let (rate, delay) = seat.get_rate();
|
let (rate, delay) = seat.get_rate();
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,18 @@ impl ConfigProxyHandler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn handle_window_send_to_scratchpad(
|
||||||
|
&self,
|
||||||
|
window: Window,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<(), CphError> {
|
||||||
|
self.state.with_linear_layout_animations(|| {
|
||||||
|
let window = self.get_window(window)?;
|
||||||
|
self.state.send_to_scratchpad(name, window);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn handle_window_exists(&self, window: Window) {
|
pub(super) fn handle_window_exists(&self, window: Window) {
|
||||||
self.respond(Response::WindowExists {
|
self.respond(Response::WindowExists {
|
||||||
exists: self.get_window(window).is_ok(),
|
exists: self.get_window(window).is_ok(),
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use {
|
||||||
clmm_sandboxed::ClmMatchSandboxed,
|
clmm_sandboxed::ClmMatchSandboxed,
|
||||||
clmm_string::{
|
clmm_string::{
|
||||||
ClmMatchComm, ClmMatchExe, ClmMatchSandboxAppId, ClmMatchSandboxEngine,
|
ClmMatchComm, ClmMatchExe, ClmMatchSandboxAppId, ClmMatchSandboxEngine,
|
||||||
ClmMatchSandboxInstanceId,
|
ClmMatchSandboxInstanceId, ClmMatchTag,
|
||||||
},
|
},
|
||||||
clmm_uid::ClmMatchUid,
|
clmm_uid::ClmMatchUid,
|
||||||
},
|
},
|
||||||
|
|
@ -62,6 +62,7 @@ pub struct RootMatchers {
|
||||||
pid: ClmRootMatcherMap<ClmMatchPid>,
|
pid: ClmRootMatcherMap<ClmMatchPid>,
|
||||||
comm: ClmRootMatcherMap<ClmMatchComm>,
|
comm: ClmRootMatcherMap<ClmMatchComm>,
|
||||||
exe: ClmRootMatcherMap<ClmMatchExe>,
|
exe: ClmRootMatcherMap<ClmMatchExe>,
|
||||||
|
tag: ClmRootMatcherMap<ClmMatchTag>,
|
||||||
id: ClmRootMatcherMap<ClmMatchId>,
|
id: ClmRootMatcherMap<ClmMatchId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,6 +75,7 @@ impl RootMatchers {
|
||||||
self.pid.clear();
|
self.pid.clear();
|
||||||
self.comm.clear();
|
self.comm.clear();
|
||||||
self.exe.clear();
|
self.exe.clear();
|
||||||
|
self.tag.clear();
|
||||||
self.id.clear();
|
self.id.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -185,6 +187,7 @@ impl ClMatcherManager {
|
||||||
unconditional!(pid);
|
unconditional!(pid);
|
||||||
unconditional!(comm);
|
unconditional!(comm);
|
||||||
unconditional!(exe);
|
unconditional!(exe);
|
||||||
|
unconditional!(tag);
|
||||||
unconditional!(id);
|
unconditional!(id);
|
||||||
fixed!(sandboxed);
|
fixed!(sandboxed);
|
||||||
fixed!(is_xwayland);
|
fixed!(is_xwayland);
|
||||||
|
|
@ -228,6 +231,9 @@ impl ClMatcherManager {
|
||||||
self.root(ClmMatchExe::new(string))
|
self.root(ClmMatchExe::new(string))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn tag(&self, string: CritLiteralOrRegex) -> Rc<ClmUpstreamNode> {
|
||||||
|
self.root(ClmMatchTag::new(string))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ClientTargetOwner {
|
pub struct ClientTargetOwner {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ pub type ClmMatchString<T> = CritMatchString<Client, T>;
|
||||||
pub type ClmMatchSandboxEngine = ClmMatchString<ClientMetadataAccess<SandboxEngineField>>;
|
pub type ClmMatchSandboxEngine = ClmMatchString<ClientMetadataAccess<SandboxEngineField>>;
|
||||||
pub type ClmMatchSandboxAppId = ClmMatchString<ClientMetadataAccess<SandboxAppIdField>>;
|
pub type ClmMatchSandboxAppId = ClmMatchString<ClientMetadataAccess<SandboxAppIdField>>;
|
||||||
pub type ClmMatchSandboxInstanceId = ClmMatchString<ClientMetadataAccess<SandboxInstanceIdField>>;
|
pub type ClmMatchSandboxInstanceId = ClmMatchString<ClientMetadataAccess<SandboxInstanceIdField>>;
|
||||||
|
pub type ClmMatchTag = ClmMatchString<ClientMetadataAccess<TagField>>;
|
||||||
pub type ClmMatchComm = ClmMatchString<CommAccess>;
|
pub type ClmMatchComm = ClmMatchString<CommAccess>;
|
||||||
pub type ClmMatchExe = ClmMatchString<ExeAccess>;
|
pub type ClmMatchExe = ClmMatchString<ExeAccess>;
|
||||||
|
|
||||||
|
|
@ -31,6 +32,7 @@ trait ClientMetadataField: Sized + 'static {
|
||||||
pub struct SandboxEngineField;
|
pub struct SandboxEngineField;
|
||||||
pub struct SandboxAppIdField;
|
pub struct SandboxAppIdField;
|
||||||
pub struct SandboxInstanceIdField;
|
pub struct SandboxInstanceIdField;
|
||||||
|
pub struct TagField;
|
||||||
|
|
||||||
impl<T> StringAccess<Client> for ClientMetadataAccess<T>
|
impl<T> StringAccess<Client> for ClientMetadataAccess<T>
|
||||||
where
|
where
|
||||||
|
|
@ -81,6 +83,18 @@ impl ClientMetadataField for SandboxInstanceIdField {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ClientMetadataField for TagField {
|
||||||
|
fn field(meta: &ClientMetadata) -> &Option<String> {
|
||||||
|
&meta.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nodes(
|
||||||
|
roots: &RootMatchers,
|
||||||
|
) -> &ClmRootMatcherMap<ClmMatchString<ClientMetadataAccess<Self>>> {
|
||||||
|
&roots.tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl StringAccess<Client> for CommAccess {
|
impl StringAccess<Client> for CommAccess {
|
||||||
fn with_string(data: &Client, f: impl FnOnce(&str) -> bool) -> bool {
|
fn with_string(data: &Client, f: impl FnOnce(&str) -> bool) -> bool {
|
||||||
f(&data.pid_info.comm)
|
f(&data.pid_info.comm)
|
||||||
|
|
|
||||||
|
|
@ -1364,25 +1364,25 @@ impl WlSurface {
|
||||||
let bounds = self.toplevel.get().and_then(|tl| tl.tl_render_bounds());
|
let bounds = self.toplevel.get().and_then(|tl| tl.tl_render_bounds());
|
||||||
let pos = self.buffer_abs_pos.get();
|
let pos = self.buffer_abs_pos.get();
|
||||||
let apply_damage = |pos: Rect| {
|
let apply_damage = |pos: Rect| {
|
||||||
if pending.damage_full {
|
let clip_damage = |mut damage: Rect| {
|
||||||
let mut damage = pos;
|
damage = damage.intersect(pos);
|
||||||
if let Some(bounds) = bounds {
|
if let Some(bounds) = bounds {
|
||||||
damage = damage.intersect(bounds);
|
damage = damage.intersect(bounds);
|
||||||
}
|
}
|
||||||
self.client.state.damage(damage);
|
damage
|
||||||
|
};
|
||||||
|
if pending.damage_full {
|
||||||
|
self.client.state.damage(clip_damage(pos));
|
||||||
} else {
|
} else {
|
||||||
let matrix = self.damage_matrix.get();
|
let matrix = self.damage_matrix.get();
|
||||||
if let Some(buffer) = self.buffer.get() {
|
if let Some(buffer) = self.buffer.get() {
|
||||||
for damage in &pending.buffer_damage {
|
for damage in &pending.buffer_damage {
|
||||||
let mut damage = matrix.apply(
|
let damage = matrix.apply(
|
||||||
pos.x1(),
|
pos.x1(),
|
||||||
pos.y1(),
|
pos.y1(),
|
||||||
damage.intersect(buffer.buffer.buf.rect),
|
damage.intersect(buffer.buffer.buf.rect),
|
||||||
);
|
);
|
||||||
if let Some(bounds) = bounds {
|
self.client.state.damage(clip_damage(damage));
|
||||||
damage = damage.intersect(bounds);
|
|
||||||
}
|
|
||||||
self.client.state.damage(damage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for damage in &pending.surface_damage {
|
for damage in &pending.surface_damage {
|
||||||
|
|
@ -1394,8 +1394,7 @@ impl WlSurface {
|
||||||
let y2 = (damage.y2() + scale - 1) / scale;
|
let y2 = (damage.y2() + scale - 1) / scale;
|
||||||
damage = Rect::new_saturating(x1, y1, x2, y2);
|
damage = Rect::new_saturating(x1, y1, x2, y2);
|
||||||
}
|
}
|
||||||
damage = damage.intersect(bounds.unwrap_or(pos));
|
self.client.state.damage(clip_damage(damage));
|
||||||
self.client.state.damage(damage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -272,6 +272,27 @@ impl TestConfig {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn send_to_scratchpad(&self, seat: SeatId, name: &str) -> TestResult {
|
||||||
|
self.send(ClientMessage::SeatSendToScratchpad {
|
||||||
|
seat: Seat(seat.raw() as _),
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_scratchpad(&self, seat: SeatId, name: &str) -> TestResult {
|
||||||
|
self.send(ClientMessage::SeatToggleScratchpad {
|
||||||
|
seat: Seat(seat.raw() as _),
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cycle_scratchpad(&self, seat: SeatId, name: &str) -> TestResult {
|
||||||
|
self.send(ClientMessage::SeatCycleScratchpad {
|
||||||
|
seat: Seat(seat.raw() as _),
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn clear(&self) {
|
fn clear(&self) {
|
||||||
unsafe {
|
unsafe {
|
||||||
if let Some(srv) = self.srv.take() {
|
if let Some(srv) = self.srv.take() {
|
||||||
|
|
@ -319,6 +340,10 @@ impl TestConfig {
|
||||||
pub fn set_show_titles(&self, show: bool) -> TestResult {
|
pub fn set_show_titles(&self, show: bool) -> TestResult {
|
||||||
self.send(ClientMessage::SetShowTitles { show })
|
self.send(ClientMessage::SetShowTitles { show })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_autotile(&self, enabled: bool) -> TestResult {
|
||||||
|
self.send(ClientMessage::SetAutotile { enabled })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for TestConfig {
|
impl Drop for TestConfig {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,17 @@ impl TestViewport {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn unset_source(&self) -> Result<(), TestError> {
|
||||||
|
self.tran.send(SetSource {
|
||||||
|
self_id: self.id,
|
||||||
|
x: Fixed::from_int(-1),
|
||||||
|
y: Fixed::from_int(-1),
|
||||||
|
width: Fixed::from_int(-1),
|
||||||
|
height: Fixed::from_int(-1),
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_destination(&self, width: i32, height: i32) -> Result<(), TestError> {
|
pub fn set_destination(&self, width: i32, height: i32) -> Result<(), TestError> {
|
||||||
self.tran.send(SetDestination {
|
self.tran.send(SetDestination {
|
||||||
self_id: self.id,
|
self_id: self.id,
|
||||||
|
|
@ -37,6 +48,15 @@ impl TestViewport {
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn unset_destination(&self) -> Result<(), TestError> {
|
||||||
|
self.tran.send(SetDestination {
|
||||||
|
self_id: self.id,
|
||||||
|
width: -1,
|
||||||
|
height: -1,
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for TestViewport {
|
impl Drop for TestViewport {
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,8 @@ mod t0051_pointer_warp;
|
||||||
mod t0052_bar;
|
mod t0052_bar;
|
||||||
mod t0053_theme;
|
mod t0053_theme;
|
||||||
mod t0054_subsurface_already_attached;
|
mod t0054_subsurface_already_attached;
|
||||||
|
mod t0055_autotiling;
|
||||||
|
mod t0055_scratchpad;
|
||||||
|
|
||||||
pub trait TestCase: Sync {
|
pub trait TestCase: Sync {
|
||||||
fn name(&self) -> &'static str;
|
fn name(&self) -> &'static str;
|
||||||
|
|
@ -158,5 +160,7 @@ pub fn tests() -> Vec<&'static dyn TestCase> {
|
||||||
t0052_bar,
|
t0052_bar,
|
||||||
t0053_theme,
|
t0053_theme,
|
||||||
t0054_subsurface_already_attached,
|
t0054_subsurface_already_attached,
|
||||||
|
t0055_autotiling,
|
||||||
|
t0055_scratchpad,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
it::{test_error::TestError, testrun::TestRun},
|
it::{test_error::TestError, testrun::TestRun},
|
||||||
rect::Rect,
|
|
||||||
tree::Node,
|
tree::Node,
|
||||||
},
|
},
|
||||||
std::rc::Rc,
|
std::rc::Rc,
|
||||||
|
|
@ -11,29 +10,19 @@ testcase!();
|
||||||
|
|
||||||
/// Create and map a single surface
|
/// Create and map a single surface
|
||||||
async fn test(run: Rc<TestRun>) -> Result<(), TestError> {
|
async fn test(run: Rc<TestRun>) -> Result<(), TestError> {
|
||||||
run.backend.install_default()?;
|
let ds = run.create_default_setup().await?;
|
||||||
|
|
||||||
let client = run.create_client().await?;
|
let client = run.create_client().await?;
|
||||||
|
|
||||||
let window = client.create_window().await?;
|
let window = client.create_window().await?;
|
||||||
window.map().await?;
|
window.map().await?;
|
||||||
|
|
||||||
tassert_eq!(window.tl.core.width.get(), 800);
|
let workspace_rect = ds.output.workspace_rect.get();
|
||||||
tassert_eq!(
|
|
||||||
window.tl.core.height.get(),
|
|
||||||
600 - 2 * run.state.theme.title_plus_underline_height()
|
|
||||||
);
|
|
||||||
|
|
||||||
tassert_eq!(
|
tassert_eq!(window.tl.core.width.get(), workspace_rect.width());
|
||||||
window.tl.server.node_absolute_position(),
|
tassert_eq!(window.tl.core.height.get(), workspace_rect.height());
|
||||||
Rect::new_sized(
|
|
||||||
0,
|
tassert_eq!(window.tl.server.node_absolute_position(), workspace_rect);
|
||||||
2 * run.state.theme.title_plus_underline_height(),
|
|
||||||
window.tl.core.width.get(),
|
|
||||||
window.tl.core.height.get(),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ testcase!();
|
||||||
|
|
||||||
/// Create and map two surfaces
|
/// Create and map two surfaces
|
||||||
async fn test(run: Rc<TestRun>) -> Result<(), TestError> {
|
async fn test(run: Rc<TestRun>) -> Result<(), TestError> {
|
||||||
run.backend.install_default()?;
|
let ds = run.create_default_setup().await?;
|
||||||
|
|
||||||
let client = run.create_client().await?;
|
let client = run.create_client().await?;
|
||||||
|
|
||||||
|
|
@ -21,17 +21,30 @@ async fn test(run: Rc<TestRun>) -> Result<(), TestError> {
|
||||||
let window2 = client.create_window().await?;
|
let window2 = client.create_window().await?;
|
||||||
window2.map().await?;
|
window2.map().await?;
|
||||||
|
|
||||||
let otop = 2 * run.state.theme.title_plus_underline_height();
|
let workspace_rect = ds.output.workspace_rect.get();
|
||||||
let bw = run.state.theme.sizes.border_width.get();
|
let bw = run.state.theme.sizes.border_width.get();
|
||||||
|
let child_width = (workspace_rect.width() - bw) / 2;
|
||||||
|
|
||||||
tassert_eq!(
|
tassert_eq!(
|
||||||
window.tl.server.node_absolute_position(),
|
window.tl.server.node_absolute_position(),
|
||||||
Rect::new_sized(0, otop, (800 - bw) / 2, 600 - otop).unwrap()
|
Rect::new_sized(
|
||||||
|
workspace_rect.x1(),
|
||||||
|
workspace_rect.y1(),
|
||||||
|
child_width,
|
||||||
|
workspace_rect.height(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
tassert_eq!(
|
tassert_eq!(
|
||||||
window2.tl.server.node_absolute_position(),
|
window2.tl.server.node_absolute_position(),
|
||||||
Rect::new_sized((800 - bw) / 2 + bw, otop, (800 - bw) / 2, 600 - otop).unwrap()
|
Rect::new_sized(
|
||||||
|
workspace_rect.x1() + child_width + bw,
|
||||||
|
workspace_rect.y1(),
|
||||||
|
child_width,
|
||||||
|
workspace_rect.height(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -48,13 +48,18 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
|
|
||||||
let mono_container = w_mono2.tl.container_parent()?;
|
let mono_container = w_mono2.tl.container_parent()?;
|
||||||
let container_pos = mono_container.tl_data().pos.get();
|
let container_pos = mono_container.tl_data().pos.get();
|
||||||
let w_mono1_title = mono_container.render_data.borrow_mut().title_rects[0]
|
let (tab_x, tab_y) = {
|
||||||
.move_(container_pos.x1(), container_pos.y1());
|
let tab_bar = mono_container.tab_bar.borrow();
|
||||||
ds.mouse.abs(
|
let Some(tab_bar) = tab_bar.as_ref() else {
|
||||||
&ds.connector,
|
bail!("no tab bar");
|
||||||
w_mono1_title.x1() as _,
|
};
|
||||||
w_mono1_title.y1() as _,
|
let w_mono1_title = &tab_bar.entries[0];
|
||||||
);
|
(
|
||||||
|
container_pos.x1() + w_mono1_title.x.get() + w_mono1_title.width.get() / 2,
|
||||||
|
container_pos.y1() + tab_bar.height / 2,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
ds.mouse.abs(&ds.connector, tab_x as _, tab_y as _);
|
||||||
|
|
||||||
client.sync().await;
|
client.sync().await;
|
||||||
tassert!(enters.next().is_err());
|
tassert!(enters.next().is_err());
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,18 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
|
|
||||||
let container = w_mono2.tl.container_parent()?;
|
let container = w_mono2.tl.container_parent()?;
|
||||||
let pos = container.tl_data().pos.get();
|
let pos = container.tl_data().pos.get();
|
||||||
let w_mono1_title = container.render_data.borrow_mut().title_rects[0].move_(pos.x1(), pos.y1());
|
let (tab_x, tab_y) = {
|
||||||
ds.mouse.abs(
|
let tab_bar = container.tab_bar.borrow();
|
||||||
&ds.connector,
|
let Some(tab_bar) = tab_bar.as_ref() else {
|
||||||
w_mono1_title.x1() as f64,
|
bail!("no tab bar");
|
||||||
w_mono1_title.y1() as f64,
|
};
|
||||||
);
|
let w_mono1_title = &tab_bar.entries[0];
|
||||||
|
(
|
||||||
|
pos.x1() + w_mono1_title.x.get() + w_mono1_title.width.get() / 2,
|
||||||
|
pos.y1() + tab_bar.height / 2,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
ds.mouse.abs(&ds.connector, tab_x as f64, tab_y as f64);
|
||||||
client.sync().await;
|
client.sync().await;
|
||||||
|
|
||||||
let enters = dss.kb.enter.expect()?;
|
let enters = dss.kb.enter.expect()?;
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -2,7 +2,7 @@ use {
|
||||||
crate::{
|
crate::{
|
||||||
ifs::wl_surface::xdg_surface::xdg_toplevel::STATE_SUSPENDED,
|
ifs::wl_surface::xdg_surface::xdg_toplevel::STATE_SUSPENDED,
|
||||||
it::{
|
it::{
|
||||||
test_error::TestResult,
|
test_error::{TestErrorExt, TestResult},
|
||||||
test_utils::{
|
test_utils::{
|
||||||
test_ouput_node_ext::TestOutputNodeExt, test_toplevel_node_ext::TestToplevelNodeExt,
|
test_ouput_node_ext::TestOutputNodeExt, test_toplevel_node_ext::TestToplevelNodeExt,
|
||||||
},
|
},
|
||||||
|
|
@ -10,7 +10,7 @@ use {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
isnt::std_1::collections::IsntHashSetExt,
|
isnt::std_1::collections::IsntHashSetExt,
|
||||||
std::rc::Rc,
|
std::{rc::Rc, time::Duration},
|
||||||
};
|
};
|
||||||
|
|
||||||
testcase!();
|
testcase!();
|
||||||
|
|
@ -19,6 +19,7 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
let ds = run.create_default_setup().await?;
|
let ds = run.create_default_setup().await?;
|
||||||
|
|
||||||
let client = run.create_client().await?;
|
let client = run.create_client().await?;
|
||||||
|
let default_seat = client.get_default_seat().await?;
|
||||||
|
|
||||||
let win1 = client.create_window().await?;
|
let win1 = client.create_window().await?;
|
||||||
win1.set_color(255, 0, 0, 255);
|
win1.set_color(255, 0, 0, 255);
|
||||||
|
|
@ -44,5 +45,23 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
client.sync().await;
|
client.sync().await;
|
||||||
tassert!(win2.tl.core.states.borrow().not_contains(&STATE_SUSPENDED));
|
tassert!(win2.tl.core.states.borrow().not_contains(&STATE_SUSPENDED));
|
||||||
|
|
||||||
|
let leaves = default_seat.kb.leave.expect()?;
|
||||||
|
let enters = default_seat.kb.enter.expect()?;
|
||||||
|
|
||||||
|
run.cfg.set_idle(Duration::from_micros(100))?;
|
||||||
|
run.cfg.set_idle_grace_period(Duration::from_secs(0))?;
|
||||||
|
run.state.wheel.timeout(3).await?;
|
||||||
|
|
||||||
|
client.sync().await;
|
||||||
|
tassert!(win2.tl.core.states.borrow().contains(&STATE_SUSPENDED));
|
||||||
|
let leave = leaves.next().with_context(|| "no leave on suspend")?;
|
||||||
|
tassert_eq!(leave.surface, win2.surface.id);
|
||||||
|
|
||||||
|
ds.mouse.rel(1.0, 1.0);
|
||||||
|
client.sync().await;
|
||||||
|
tassert!(win2.tl.core.states.borrow().not_contains(&STATE_SUSPENDED));
|
||||||
|
let enter = enters.next().with_context(|| "no enter on restore")?;
|
||||||
|
tassert_eq!(enter.surface, win2.surface.id);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -308,9 +308,8 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
let output_damage = connector_data.damage.borrow();
|
let output_damage = connector_data.damage.borrow();
|
||||||
tassert!(!output_damage.is_empty());
|
tassert!(!output_damage.is_empty());
|
||||||
|
|
||||||
// Buffer damage is transformed by the damage matrix which includes the surface position
|
// The test window maps its 1x1 buffer through a viewport to the full window size.
|
||||||
// The buffer damage (0,0,1,1) should be transformed to surface coordinates
|
let expected_buffer_damage = surface_pos;
|
||||||
let expected_buffer_damage = buffer_damage.move_(surface_pos.x1(), surface_pos.y1());
|
|
||||||
|
|
||||||
// Find the exact output damage that matches our expected buffer damage
|
// Find the exact output damage that matches our expected buffer damage
|
||||||
let mut found_exact_buffer_damage = false;
|
let mut found_exact_buffer_damage = false;
|
||||||
|
|
@ -331,10 +330,12 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
// Test 7: Check output damage from existing window's viewport (which already has scaling)
|
// Test 7: Check output damage from existing window's viewport (which already has scaling)
|
||||||
connector_data.damage.borrow_mut().clear();
|
connector_data.damage.borrow_mut().clear();
|
||||||
|
|
||||||
// The existing window was created with create_surface_ext() which automatically creates a viewport
|
// The existing window was created with create_surface_ext() which automatically creates a viewport.
|
||||||
// Let's verify that the viewport's existing scaling affects buffer damage correctly
|
// Commit the viewport size change separately; that commit intentionally damages the old/new extents.
|
||||||
// First, let's modify the viewport scaling that already exists on the window
|
window.surface.viewport.set_destination(150, 100)?;
|
||||||
window.surface.viewport.set_destination(150, 100)?; // Change scaling to 150x100
|
window.surface.commit()?;
|
||||||
|
client.sync().await;
|
||||||
|
connector_data.damage.borrow_mut().clear();
|
||||||
|
|
||||||
// Add buffer damage to test viewport scaling coordinate transformation
|
// Add buffer damage to test viewport scaling coordinate transformation
|
||||||
window.surface.damage_buffer(0, 0, 1, 1)?; // Damage entire 1x1 buffer
|
window.surface.damage_buffer(0, 0, 1, 1)?; // Damage entire 1x1 buffer
|
||||||
|
|
@ -346,8 +347,8 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
let output_damage = connector_data.damage.borrow();
|
let output_damage = connector_data.damage.borrow();
|
||||||
tassert!(!output_damage.is_empty());
|
tassert!(!output_damage.is_empty());
|
||||||
|
|
||||||
// With viewporter scaling, the 1x1 buffer damage should scale to 150x100
|
// With viewporter scaling, the 1x1 buffer damage should scale to the viewport destination.
|
||||||
// and be moved by surface position (0, 36) to get output coordinates (0, 36, 150, 136)
|
let surface_pos = window.surface.server.buffer_abs_pos.get();
|
||||||
let expected_scaled_damage = Rect::new_sized(0, 0, 150, 100).unwrap();
|
let expected_scaled_damage = Rect::new_sized(0, 0, 150, 100).unwrap();
|
||||||
let expected_output_damage =
|
let expected_output_damage =
|
||||||
expected_scaled_damage.move_(surface_pos.x1(), surface_pos.y1());
|
expected_scaled_damage.move_(surface_pos.x1(), surface_pos.y1());
|
||||||
|
|
@ -402,8 +403,9 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
rotation_window.map().await?;
|
rotation_window.map().await?;
|
||||||
client.sync().await;
|
client.sync().await;
|
||||||
|
|
||||||
// Disable viewporter by setting destination to 0x0 to rely purely on buffer dimensions
|
// Disable viewporter to rely purely on buffer dimensions.
|
||||||
rotation_window.surface.viewport.set_destination(0, 0)?; // Disable viewporter
|
rotation_window.surface.viewport.unset_source()?;
|
||||||
|
rotation_window.surface.viewport.unset_destination()?;
|
||||||
|
|
||||||
// Use a rectangular buffer (4x2) so rotation has a visible geometric effect
|
// Use a rectangular buffer (4x2) so rotation has a visible geometric effect
|
||||||
// Attach AFTER mapping to avoid being overwritten by map()'s single-pixel buffer
|
// Attach AFTER mapping to avoid being overwritten by map()'s single-pixel buffer
|
||||||
|
|
|
||||||
58
src/it/tests/t0055_autotiling.rs
Normal file
58
src/it/tests/t0055_autotiling.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
use {
|
||||||
|
crate::{
|
||||||
|
it::{test_error::TestResult, testrun::TestRun},
|
||||||
|
tree::{ContainerSplit, Node, ToplevelNodeBase},
|
||||||
|
},
|
||||||
|
std::rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
|
testcase!();
|
||||||
|
|
||||||
|
async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
|
run.backend.install_default()?;
|
||||||
|
run.cfg.set_autotile(true)?;
|
||||||
|
|
||||||
|
let client = run.create_client().await?;
|
||||||
|
|
||||||
|
let win1 = client.create_window().await?;
|
||||||
|
win1.map().await?;
|
||||||
|
let root = win1.tl.container_parent()?;
|
||||||
|
tassert_eq!(root.split.get(), ContainerSplit::Horizontal);
|
||||||
|
|
||||||
|
let win2 = client.create_window().await?;
|
||||||
|
win2.map().await?;
|
||||||
|
client.sync().await;
|
||||||
|
|
||||||
|
tassert_eq!(root.split.get(), ContainerSplit::Horizontal);
|
||||||
|
tassert_eq!(win1.tl.container_parent()?.node_id(), root.node_id());
|
||||||
|
tassert_eq!(win2.tl.container_parent()?.node_id(), root.node_id());
|
||||||
|
|
||||||
|
let win3 = client.create_window().await?;
|
||||||
|
win3.map().await?;
|
||||||
|
client.sync().await;
|
||||||
|
|
||||||
|
let v_group = win3.tl.container_parent()?;
|
||||||
|
tassert_eq!(root.split.get(), ContainerSplit::Horizontal);
|
||||||
|
tassert_eq!(v_group.split.get(), ContainerSplit::Vertical);
|
||||||
|
tassert_eq!(win2.tl.container_parent()?.node_id(), v_group.node_id());
|
||||||
|
|
||||||
|
let win4 = client.create_window().await?;
|
||||||
|
win4.map().await?;
|
||||||
|
client.sync().await;
|
||||||
|
|
||||||
|
let h_group = win4.tl.container_parent()?;
|
||||||
|
tassert_eq!(h_group.split.get(), ContainerSplit::Horizontal);
|
||||||
|
tassert_eq!(win3.tl.container_parent()?.node_id(), h_group.node_id());
|
||||||
|
let h_parent = match h_group
|
||||||
|
.tl_data()
|
||||||
|
.parent
|
||||||
|
.get()
|
||||||
|
.and_then(|p| p.node_into_container())
|
||||||
|
{
|
||||||
|
Some(parent) => parent,
|
||||||
|
None => bail!("autotile group does not have a container parent"),
|
||||||
|
};
|
||||||
|
tassert_eq!(h_parent.node_id(), v_group.node_id());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
107
src/it/tests/t0055_scratchpad.rs
Normal file
107
src/it/tests/t0055_scratchpad.rs
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
use {
|
||||||
|
crate::{
|
||||||
|
it::{test_error::TestResult, testrun::TestRun},
|
||||||
|
tree::{Node, ToplevelNodeBase},
|
||||||
|
},
|
||||||
|
std::rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
|
testcase!();
|
||||||
|
|
||||||
|
async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
|
let ds = run.create_default_setup().await?;
|
||||||
|
|
||||||
|
let client = run.create_client().await?;
|
||||||
|
let win1 = client.create_window().await?;
|
||||||
|
win1.map2().await?;
|
||||||
|
let win2 = client.create_window().await?;
|
||||||
|
win2.map2().await?;
|
||||||
|
|
||||||
|
run.cfg.send_to_scratchpad(ds.seat.id(), "term")?;
|
||||||
|
client.sync().await;
|
||||||
|
tassert!(win1.tl.server.node_visible());
|
||||||
|
tassert!(!win2.tl.server.node_visible());
|
||||||
|
|
||||||
|
run.cfg.show_workspace(ds.seat.id(), "2")?;
|
||||||
|
run.cfg.toggle_scratchpad(ds.seat.id(), "term")?;
|
||||||
|
client.sync().await;
|
||||||
|
tassert!(win2.tl.server.node_visible());
|
||||||
|
tassert_eq!(ds.output.workspace.get().unwrap().name.as_str(), "2");
|
||||||
|
|
||||||
|
run.cfg.toggle_scratchpad(ds.seat.id(), "term")?;
|
||||||
|
client.sync().await;
|
||||||
|
tassert!(!win2.tl.server.node_visible());
|
||||||
|
|
||||||
|
run.cfg.toggle_scratchpad(ds.seat.id(), "term")?;
|
||||||
|
client.sync().await;
|
||||||
|
tassert!(win2.tl.server.node_visible());
|
||||||
|
tassert_eq!(ds.output.workspace.get().unwrap().name.as_str(), "2");
|
||||||
|
|
||||||
|
run.cfg.show_workspace(ds.seat.id(), "3")?;
|
||||||
|
client.sync().await;
|
||||||
|
tassert!(!win2.tl.server.node_visible());
|
||||||
|
|
||||||
|
run.cfg.toggle_scratchpad(ds.seat.id(), "term")?;
|
||||||
|
client.sync().await;
|
||||||
|
tassert!(win2.tl.server.node_visible());
|
||||||
|
tassert_eq!(ds.output.workspace.get().unwrap().name.as_str(), "3");
|
||||||
|
// Scratchpad windows are always shown floating.
|
||||||
|
tassert!(win2.tl.server.tl_data().parent_is_float.get());
|
||||||
|
|
||||||
|
// Park win2 again, then build a multi-window scratchpad and cycle it.
|
||||||
|
run.cfg.toggle_scratchpad(ds.seat.id(), "term")?;
|
||||||
|
client.sync().await;
|
||||||
|
tassert!(!win2.tl.server.node_visible());
|
||||||
|
|
||||||
|
// Build a three-window scratchpad. Each window is focused right after it is
|
||||||
|
// mapped, so sending the focused window parks them in a known order.
|
||||||
|
let cyc1 = client.create_window().await?;
|
||||||
|
cyc1.map2().await?;
|
||||||
|
run.cfg.send_to_scratchpad(ds.seat.id(), "cyc")?;
|
||||||
|
let cyc2 = client.create_window().await?;
|
||||||
|
cyc2.map2().await?;
|
||||||
|
run.cfg.send_to_scratchpad(ds.seat.id(), "cyc")?;
|
||||||
|
let cyc3 = client.create_window().await?;
|
||||||
|
cyc3.map2().await?;
|
||||||
|
run.cfg.send_to_scratchpad(ds.seat.id(), "cyc")?;
|
||||||
|
client.sync().await;
|
||||||
|
tassert!(!cyc1.tl.server.node_visible());
|
||||||
|
tassert!(!cyc2.tl.server.node_visible());
|
||||||
|
tassert!(!cyc3.tl.server.node_visible());
|
||||||
|
|
||||||
|
// Nothing shown: cycle brings up the first window (insertion order: cyc1).
|
||||||
|
run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?;
|
||||||
|
client.sync().await;
|
||||||
|
tassert!(cyc1.tl.server.node_visible());
|
||||||
|
tassert!(!cyc2.tl.server.node_visible());
|
||||||
|
tassert!(!cyc3.tl.server.node_visible());
|
||||||
|
// Scratchpad windows are always shown floating.
|
||||||
|
tassert!(cyc1.tl.server.tl_data().parent_is_float.get());
|
||||||
|
|
||||||
|
// Cycle advances one at a time.
|
||||||
|
run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?;
|
||||||
|
client.sync().await;
|
||||||
|
tassert!(!cyc1.tl.server.node_visible());
|
||||||
|
tassert!(cyc2.tl.server.node_visible());
|
||||||
|
tassert!(!cyc3.tl.server.node_visible());
|
||||||
|
|
||||||
|
run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?;
|
||||||
|
client.sync().await;
|
||||||
|
tassert!(!cyc1.tl.server.node_visible());
|
||||||
|
tassert!(!cyc2.tl.server.node_visible());
|
||||||
|
tassert!(cyc3.tl.server.node_visible());
|
||||||
|
|
||||||
|
// On the final window, the next cycle hides everything.
|
||||||
|
run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?;
|
||||||
|
client.sync().await;
|
||||||
|
tassert!(!cyc1.tl.server.node_visible());
|
||||||
|
tassert!(!cyc2.tl.server.node_visible());
|
||||||
|
tassert!(!cyc3.tl.server.node_visible());
|
||||||
|
|
||||||
|
// And it wraps back to the first window.
|
||||||
|
run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?;
|
||||||
|
client.sync().await;
|
||||||
|
tassert!(cyc1.tl.server.node_visible());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -99,6 +99,7 @@ mod scale;
|
||||||
mod screenshoter;
|
mod screenshoter;
|
||||||
mod sighand;
|
mod sighand;
|
||||||
mod state;
|
mod state;
|
||||||
|
mod tagged_acceptor;
|
||||||
mod tasks;
|
mod tasks;
|
||||||
mod text;
|
mod text;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
|
|
||||||
26
src/state.rs
26
src/state.rs
|
|
@ -87,6 +87,7 @@ use {
|
||||||
pr_caps::PrCapsThread,
|
pr_caps::PrCapsThread,
|
||||||
rect::Rect,
|
rect::Rect,
|
||||||
scale::Scale,
|
scale::Scale,
|
||||||
|
tagged_acceptor::TaggedAcceptors,
|
||||||
theme::Theme,
|
theme::Theme,
|
||||||
time::Time,
|
time::Time,
|
||||||
tree::{
|
tree::{
|
||||||
|
|
@ -185,6 +186,7 @@ pub struct State {
|
||||||
pub run_args: RunArgs,
|
pub run_args: RunArgs,
|
||||||
pub xwayland: XWaylandState,
|
pub xwayland: XWaylandState,
|
||||||
pub acceptor: CloneCell<Option<Rc<Acceptor>>>,
|
pub acceptor: CloneCell<Option<Rc<Acceptor>>>,
|
||||||
|
pub tagged_acceptors: TaggedAcceptors,
|
||||||
pub serial: NumCell<u64>,
|
pub serial: NumCell<u64>,
|
||||||
pub run_toplevel: Rc<RunToplevel>,
|
pub run_toplevel: Rc<RunToplevel>,
|
||||||
pub config_dir: Option<String>,
|
pub config_dir: Option<String>,
|
||||||
|
|
@ -283,6 +285,7 @@ pub struct State {
|
||||||
pub bo_drop_queue: Rc<ObjectDropQueue<Rc<dyn BufferObject>>>,
|
pub bo_drop_queue: Rc<ObjectDropQueue<Rc<dyn BufferObject>>>,
|
||||||
pub virtual_outputs: VirtualOutputs,
|
pub virtual_outputs: VirtualOutputs,
|
||||||
pub clean_logs_older_than: Cell<Option<SystemTime>>,
|
pub clean_logs_older_than: Cell<Option<SystemTime>>,
|
||||||
|
pub scratchpads: RefCell<AHashMap<String, Vec<Rc<ScratchpadEntry>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl Drop for State {
|
// impl Drop for State {
|
||||||
|
|
@ -302,6 +305,27 @@ pub struct ScreenlockState {
|
||||||
pub lock: CloneCell<Option<Rc<ExtSessionLockV1>>>,
|
pub lock: CloneCell<Option<Rc<ExtSessionLockV1>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ScratchpadEntry {
|
||||||
|
node: Weak<dyn ToplevelNode>,
|
||||||
|
identifier: ToplevelIdentifier,
|
||||||
|
hidden: Cell<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScratchpadEntry {
|
||||||
|
fn alive(&self) -> bool {
|
||||||
|
self.node().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node(&self) -> Option<Rc<dyn ToplevelNode>> {
|
||||||
|
let node = self.node.upgrade()?;
|
||||||
|
if node.tl_data().identifier.get() == self.identifier {
|
||||||
|
Some(node)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct InputDeviceData {
|
pub struct InputDeviceData {
|
||||||
pub _handler: SpawnedFuture<()>,
|
pub _handler: SpawnedFuture<()>,
|
||||||
pub id: InputDeviceId,
|
pub id: InputDeviceId,
|
||||||
|
|
@ -475,6 +499,7 @@ impl State {
|
||||||
self.eng.clear();
|
self.eng.clear();
|
||||||
self.ei_acceptor.take();
|
self.ei_acceptor.take();
|
||||||
self.ei_acceptor_future.take();
|
self.ei_acceptor_future.take();
|
||||||
|
self.tagged_acceptors.clear();
|
||||||
self.ei_clients.clear();
|
self.ei_clients.clear();
|
||||||
self.slow_ei_clients.clear();
|
self.slow_ei_clients.clear();
|
||||||
self.toplevels.clear();
|
self.toplevels.clear();
|
||||||
|
|
@ -484,6 +509,7 @@ impl State {
|
||||||
self.node_at_tree.borrow_mut().clear();
|
self.node_at_tree.borrow_mut().clear();
|
||||||
self.position_hint_requests.clear();
|
self.position_hint_requests.clear();
|
||||||
self.pending_warp_mouse_to_focus.clear();
|
self.pending_warp_mouse_to_focus.clear();
|
||||||
|
self.scratchpads.borrow_mut().clear();
|
||||||
self.head_managers.clear();
|
self.head_managers.clear();
|
||||||
self.head_managers_async.clear();
|
self.head_managers_async.clear();
|
||||||
self.const_40hz_latch.clear();
|
self.const_40hz_latch.clear();
|
||||||
|
|
|
||||||
|
|
@ -859,4 +859,3 @@ mod tests {
|
||||||
assert_eq!(merged[1].node_id, NodeId(2));
|
assert_eq!(merged[1].node_id, NodeId(2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,14 @@ use {
|
||||||
tree::{
|
tree::{
|
||||||
ContainerNode, ContainerSplit, Direction, FindTreeUsecase, FloatNode, FoundNode, Node,
|
ContainerNode, ContainerSplit, Direction, FindTreeUsecase, FloatNode, FoundNode, Node,
|
||||||
OutputNode, TileState, ToplevelData, ToplevelNode, ToplevelNodeBase, WorkspaceNode,
|
OutputNode, TileState, ToplevelData, ToplevelNode, ToplevelNodeBase, WorkspaceNode,
|
||||||
WsMoveConfig, generic_node_visitor, move_ws_to_output,
|
WsMoveConfig, generic_node_visitor, move_ws_to_output, toplevel_hide_for_scratchpad,
|
||||||
|
toplevel_restore_from_scratchpad, toplevel_set_workspace,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
std::{ops::Deref, rc::Rc},
|
std::{cell::Cell, ops::Deref, rc::Rc},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::State;
|
use super::{ScratchpadEntry, State};
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn tree_changed(&self) {
|
pub fn tree_changed(&self) {
|
||||||
|
|
@ -41,19 +42,39 @@ impl State {
|
||||||
&& node.tl_data().kind.is_app_window()
|
&& node.tl_data().kind.is_app_window()
|
||||||
&& !node.tl_data().visible.get();
|
&& !node.tl_data().visible.get();
|
||||||
if animate_new_app_map {
|
if animate_new_app_map {
|
||||||
self.with_layout_animations(|| self.do_map_tiled(seat.as_deref(), node.clone()));
|
self.with_layout_animations(|| self.do_map_tiled(seat.as_deref(), node.clone(), true));
|
||||||
} else {
|
} else {
|
||||||
self.do_map_tiled(seat.as_deref(), node.clone());
|
self.do_map_tiled(seat.as_deref(), node.clone(), true);
|
||||||
}
|
}
|
||||||
self.focus_after_map(node, seat.as_deref());
|
self.focus_after_map(node, seat.as_deref());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_map_tiled(self: &Rc<Self>, seat: Option<&Rc<WlSeatGlobal>>, node: Rc<dyn ToplevelNode>) {
|
pub fn map_tiled_without_autotile(self: &Rc<Self>, node: Rc<dyn ToplevelNode>) {
|
||||||
|
let seat = self.seat_queue.last();
|
||||||
|
self.do_map_tiled(seat.as_deref(), node.clone(), false);
|
||||||
|
self.focus_after_map(node, seat.as_deref());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_map_tiled(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
seat: Option<&Rc<WlSeatGlobal>>,
|
||||||
|
node: Rc<dyn ToplevelNode>,
|
||||||
|
autotile: bool,
|
||||||
|
) {
|
||||||
let ws = self.ensure_map_workspace(seat);
|
let ws = self.ensure_map_workspace(seat);
|
||||||
self.map_tiled_on(node, &ws);
|
self.map_tiled_on_(node, &ws, autotile);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn map_tiled_on(self: &Rc<Self>, node: Rc<dyn ToplevelNode>, ws: &Rc<WorkspaceNode>) {
|
pub fn map_tiled_on(self: &Rc<Self>, node: Rc<dyn ToplevelNode>, ws: &Rc<WorkspaceNode>) {
|
||||||
|
self.map_tiled_on_(node, ws, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_tiled_on_(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
node: Rc<dyn ToplevelNode>,
|
||||||
|
ws: &Rc<WorkspaceNode>,
|
||||||
|
autotile: bool,
|
||||||
|
) {
|
||||||
if let Some(c) = ws.container.get() {
|
if let Some(c) = ws.container.get() {
|
||||||
let la = c.clone().tl_last_active_child();
|
let la = c.clone().tl_last_active_child();
|
||||||
let lap = la
|
let lap = la
|
||||||
|
|
@ -62,7 +83,11 @@ impl State {
|
||||||
.get()
|
.get()
|
||||||
.and_then(|n| n.node_into_container());
|
.and_then(|n| n.node_into_container());
|
||||||
if let Some(lap) = lap {
|
if let Some(lap) = lap {
|
||||||
lap.add_child_after(&*la, node);
|
if autotile {
|
||||||
|
lap.add_tiled_child_after(&*la, node);
|
||||||
|
} else {
|
||||||
|
lap.add_child_after(&*la, node);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
c.append_child(node);
|
c.append_child(node);
|
||||||
}
|
}
|
||||||
|
|
@ -115,6 +140,146 @@ impl State {
|
||||||
float
|
float
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn send_to_scratchpad(self: &Rc<Self>, name: &str, node: Rc<dyn ToplevelNode>) {
|
||||||
|
if node.node_is_placeholder() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let identifier = node.tl_data().identifier.get();
|
||||||
|
if !toplevel_hide_for_scratchpad(node.clone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let entry = Rc::new(ScratchpadEntry {
|
||||||
|
node: Rc::downgrade(&node),
|
||||||
|
identifier,
|
||||||
|
hidden: Cell::new(true),
|
||||||
|
});
|
||||||
|
{
|
||||||
|
let mut scratchpads = self.scratchpads.borrow_mut();
|
||||||
|
for entries in scratchpads.values_mut() {
|
||||||
|
entries.retain(|entry| entry.alive() && entry.identifier != identifier);
|
||||||
|
}
|
||||||
|
scratchpads
|
||||||
|
.entry(name.to_string())
|
||||||
|
.or_default()
|
||||||
|
.push(entry);
|
||||||
|
}
|
||||||
|
self.tree_changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_scratchpad(self: &Rc<Self>, seat: &Rc<WlSeatGlobal>, name: &str) {
|
||||||
|
let entry = {
|
||||||
|
let mut scratchpads = self.scratchpads.borrow_mut();
|
||||||
|
let Some(entries) = scratchpads.get_mut(name) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
entries.retain(|entry| entry.alive());
|
||||||
|
// Prefer the currently-shown window; otherwise act on the most recent.
|
||||||
|
entries
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|entry| !entry.hidden.get())
|
||||||
|
.or_else(|| entries.last())
|
||||||
|
.cloned()
|
||||||
|
};
|
||||||
|
let Some(entry) = entry else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if entry.hidden.get() {
|
||||||
|
self.show_scratchpad_entry(seat, name, &entry);
|
||||||
|
} else if entry.node().is_some_and(|node| !node.node_visible()) {
|
||||||
|
self.move_scratchpad_entry_to_current_workspace(seat, &entry);
|
||||||
|
} else {
|
||||||
|
self.hide_scratchpad_entry(&entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cycles through the windows of a scratchpad, one at a time:
|
||||||
|
/// nothing shown -> first window -> ... -> last window -> nothing shown.
|
||||||
|
pub fn cycle_scratchpad(self: &Rc<Self>, seat: &Rc<WlSeatGlobal>, name: &str) {
|
||||||
|
let (current, next) = {
|
||||||
|
let mut scratchpads = self.scratchpads.borrow_mut();
|
||||||
|
let Some(entries) = scratchpads.get_mut(name) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
entries.retain(|entry| entry.alive());
|
||||||
|
match entries.iter().position(|entry| !entry.hidden.get()) {
|
||||||
|
// Nothing shown yet: bring up the first window.
|
||||||
|
None => (None, entries.first().cloned()),
|
||||||
|
// Hide the shown window and advance; on the last window, `next`
|
||||||
|
// is `None`, so the scratchpad toggles off.
|
||||||
|
Some(i) => (entries.get(i).cloned(), entries.get(i + 1).cloned()),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(current) = ¤t {
|
||||||
|
self.hide_scratchpad_entry(current);
|
||||||
|
}
|
||||||
|
if let Some(next) = &next {
|
||||||
|
self.show_scratchpad_entry(seat, name, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hide_scratchpad_entry(self: &Rc<Self>, entry: &Rc<ScratchpadEntry>) {
|
||||||
|
if entry.hidden.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(node) = entry.node() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if toplevel_hide_for_scratchpad(node) {
|
||||||
|
entry.hidden.set(true);
|
||||||
|
self.tree_changed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_scratchpad_entry(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
seat: &Rc<WlSeatGlobal>,
|
||||||
|
name: &str,
|
||||||
|
entry: &Rc<ScratchpadEntry>,
|
||||||
|
) {
|
||||||
|
if !entry.hidden.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(node) = entry.node() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// Only one window of a scratchpad is visible at a time.
|
||||||
|
let siblings: Vec<_> = {
|
||||||
|
let scratchpads = self.scratchpads.borrow();
|
||||||
|
scratchpads
|
||||||
|
.get(name)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter(|sibling| !Rc::ptr_eq(sibling, entry) && !sibling.hidden.get())
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
for sibling in siblings {
|
||||||
|
self.hide_scratchpad_entry(&sibling);
|
||||||
|
}
|
||||||
|
let ws = seat.get_fallback_output().ensure_workspace();
|
||||||
|
toplevel_restore_from_scratchpad(self, node.clone(), &ws);
|
||||||
|
entry.hidden.set(false);
|
||||||
|
node.node_do_focus(seat, Direction::Unspecified);
|
||||||
|
seat.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
self.tree_changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_scratchpad_entry_to_current_workspace(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
seat: &Rc<WlSeatGlobal>,
|
||||||
|
entry: &Rc<ScratchpadEntry>,
|
||||||
|
) {
|
||||||
|
let Some(node) = entry.node() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let ws = seat.get_fallback_output().ensure_workspace();
|
||||||
|
toplevel_set_workspace(self, node.clone(), &ws);
|
||||||
|
node.node_do_focus(seat, Direction::Unspecified);
|
||||||
|
seat.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
self.tree_changed();
|
||||||
|
}
|
||||||
|
|
||||||
fn focus_after_map(&self, node: Rc<dyn ToplevelNode>, seat: Option<&Rc<WlSeatGlobal>>) {
|
fn focus_after_map(&self, node: Rc<dyn ToplevelNode>, seat: Option<&Rc<WlSeatGlobal>>) {
|
||||||
if !node.node_visible() {
|
if !node.node_visible() {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
191
src/tagged_acceptor.rs
Normal file
191
src/tagged_acceptor.rs
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
use {
|
||||||
|
crate::{
|
||||||
|
async_engine::SpawnedFuture,
|
||||||
|
client::ClientMetadata,
|
||||||
|
state::State,
|
||||||
|
utils::{
|
||||||
|
errorfmt::ErrorFmt,
|
||||||
|
numcell::NumCell,
|
||||||
|
oserror::{OsError, OsErrorExt, OsErrorExt2},
|
||||||
|
xrd::xrd,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ahash::AHashMap,
|
||||||
|
std::{
|
||||||
|
cell::{Cell, RefCell},
|
||||||
|
rc::Rc,
|
||||||
|
},
|
||||||
|
thiserror::Error,
|
||||||
|
uapi::{OwnedFd, Ustring, c, format_ustr},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum TaggedAcceptorError {
|
||||||
|
#[error("XDG_RUNTIME_DIR is not set")]
|
||||||
|
XrdNotSet,
|
||||||
|
#[error("XDG_RUNTIME_DIR ({0:?}) is too long to form a unix socket address")]
|
||||||
|
XrdTooLong(String),
|
||||||
|
#[error("Could not create a wayland socket")]
|
||||||
|
SocketFailed(#[source] OsError),
|
||||||
|
#[error("Could not stat the existing socket")]
|
||||||
|
SocketStat(#[source] OsError),
|
||||||
|
#[error("Could not start listening for incoming connections")]
|
||||||
|
ListenFailed(#[source] OsError),
|
||||||
|
#[error("Could not open the lock file")]
|
||||||
|
OpenLockFile(#[source] OsError),
|
||||||
|
#[error("Could not lock the lock file")]
|
||||||
|
LockLockFile(#[source] OsError),
|
||||||
|
#[error("Could not bind the socket to an address")]
|
||||||
|
BindFailed(#[source] OsError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct TaggedAcceptors {
|
||||||
|
acceptors: RefCell<AHashMap<String, Rc<Acceptor>>>,
|
||||||
|
next_name: NumCell<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Acceptor {
|
||||||
|
socket: AllocatedSocket,
|
||||||
|
tag: String,
|
||||||
|
state: Rc<State>,
|
||||||
|
metadata: Rc<ClientMetadata>,
|
||||||
|
future: Cell<Option<SpawnedFuture<()>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TaggedAcceptors {
|
||||||
|
pub fn clear(&self) {
|
||||||
|
let acceptors = self.acceptors.take();
|
||||||
|
for (_, acceptor) in acceptors {
|
||||||
|
acceptor.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, state: &Rc<State>, tag: &str) -> Result<Rc<String>, TaggedAcceptorError> {
|
||||||
|
let acceptors = &mut *self.acceptors.borrow_mut();
|
||||||
|
if let Some(acceptor) = acceptors.get(tag) {
|
||||||
|
return Ok(acceptor.socket.name.clone());
|
||||||
|
}
|
||||||
|
let acceptor = Rc::new(Acceptor {
|
||||||
|
socket: self.allocate_socket()?,
|
||||||
|
tag: tag.to_owned(),
|
||||||
|
state: state.clone(),
|
||||||
|
metadata: Rc::new(ClientMetadata {
|
||||||
|
tag: Some(tag.to_owned()),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
future: Default::default(),
|
||||||
|
});
|
||||||
|
log::info!("Creating tagged acceptor `{tag}`");
|
||||||
|
acceptor.future.set(Some(
|
||||||
|
state.eng.spawn("tagged accept", acceptor.clone().accept()),
|
||||||
|
));
|
||||||
|
acceptors.insert(tag.to_owned(), acceptor.clone());
|
||||||
|
Ok(acceptor.socket.name.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn allocate_socket(&self) -> Result<AllocatedSocket, TaggedAcceptorError> {
|
||||||
|
let xrd = xrd().ok_or(TaggedAcceptorError::XrdNotSet)?;
|
||||||
|
let socket = uapi::socket(c::AF_UNIX, c::SOCK_STREAM | c::SOCK_CLOEXEC, 0)
|
||||||
|
.map(Rc::new)
|
||||||
|
.map_os_err(TaggedAcceptorError::SocketFailed)?;
|
||||||
|
loop {
|
||||||
|
let i = self.next_name.fetch_add(1) + 1000;
|
||||||
|
if let Some(s) = bind_socket(&socket, &xrd, i)? {
|
||||||
|
return Ok(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Acceptor {
|
||||||
|
fn kill(&self) {
|
||||||
|
log::info!("Destroying tagged acceptor `{}`", self.tag);
|
||||||
|
self.future.take();
|
||||||
|
self.state
|
||||||
|
.tagged_acceptors
|
||||||
|
.acceptors
|
||||||
|
.borrow_mut()
|
||||||
|
.remove(&self.tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn accept(self: Rc<Self>) {
|
||||||
|
let state = &self.state;
|
||||||
|
loop {
|
||||||
|
let fd = match state.ring.accept(&self.socket.socket, c::SOCK_CLOEXEC).await {
|
||||||
|
Ok(fd) => fd,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Could not accept a client: {}", ErrorFmt(e));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let id = state.clients.id();
|
||||||
|
if let Err(e) = state.clients.spawn(id, state, fd, &self.metadata) {
|
||||||
|
log::error!("Could not spawn a client: {}", ErrorFmt(e));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AllocatedSocket {
|
||||||
|
// wayland-x
|
||||||
|
name: Rc<String>,
|
||||||
|
// /run/user/1000/wayland-x
|
||||||
|
path: Ustring,
|
||||||
|
socket: Rc<OwnedFd>,
|
||||||
|
// /run/user/1000/wayland-x.lock
|
||||||
|
lock_path: Ustring,
|
||||||
|
_lock_fd: OwnedFd,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for AllocatedSocket {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = uapi::unlink(&self.path);
|
||||||
|
let _ = uapi::unlink(&self.lock_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bind_socket(
|
||||||
|
fd: &Rc<OwnedFd>,
|
||||||
|
xrd: &str,
|
||||||
|
id: u64,
|
||||||
|
) -> Result<Option<AllocatedSocket>, TaggedAcceptorError> {
|
||||||
|
let mut addr: c::sockaddr_un = uapi::pod_zeroed();
|
||||||
|
addr.sun_family = c::AF_UNIX as _;
|
||||||
|
let name = Rc::new(format!("wayland-{}", id));
|
||||||
|
let path = format_ustr!("{}/{}", xrd, name);
|
||||||
|
let lock_path = format_ustr!("{}.lock", path.display());
|
||||||
|
if path.len() + 1 > addr.sun_path.len() {
|
||||||
|
return Err(TaggedAcceptorError::XrdTooLong(xrd.to_string()));
|
||||||
|
}
|
||||||
|
let lock_fd = uapi::open(&*lock_path, c::O_CREAT | c::O_CLOEXEC | c::O_RDWR, 0o644)
|
||||||
|
.map_os_err(TaggedAcceptorError::OpenLockFile)?;
|
||||||
|
if let Err(e) = uapi::flock(lock_fd.raw(), c::LOCK_EX | c::LOCK_NB).to_os_error() {
|
||||||
|
if e.0 == c::EWOULDBLOCK {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
return Err(TaggedAcceptorError::LockLockFile(e));
|
||||||
|
}
|
||||||
|
match uapi::lstat(&path).to_os_error() {
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!("Unlinking {}", path.display());
|
||||||
|
let _ = uapi::unlink(&path);
|
||||||
|
}
|
||||||
|
Err(OsError(c::ENOENT)) => {}
|
||||||
|
Err(e) => return Err(TaggedAcceptorError::SocketStat(e)),
|
||||||
|
}
|
||||||
|
let sun_path = uapi::as_bytes_mut(&mut addr.sun_path[..]);
|
||||||
|
sun_path[..path.len()].copy_from_slice(path.as_bytes());
|
||||||
|
sun_path[path.len()] = 0;
|
||||||
|
uapi::bind(fd.raw(), &addr).map_os_err(TaggedAcceptorError::BindFailed)?;
|
||||||
|
uapi::listen(fd.raw(), 4096).map_os_err(TaggedAcceptorError::ListenFailed)?;
|
||||||
|
Ok(Some(AllocatedSocket {
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
socket: fd.clone(),
|
||||||
|
lock_path,
|
||||||
|
_lock_fd: lock_fd,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,7 @@ use {
|
||||||
linkedlist::{LinkedList, LinkedNode, NodeRef},
|
linkedlist::{LinkedList, LinkedNode, NodeRef},
|
||||||
numcell::NumCell,
|
numcell::NumCell,
|
||||||
rc_eq::rc_eq,
|
rc_eq::rc_eq,
|
||||||
|
scroller::Scroller,
|
||||||
threshold_counter::ThresholdCounter,
|
threshold_counter::ThresholdCounter,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -154,6 +155,7 @@ pub struct ContainerNode {
|
||||||
pub child_removed: Rc<LazyEventSource>,
|
pub child_removed: Rc<LazyEventSource>,
|
||||||
pub all_children_resized: Rc<LazyEventSource>,
|
pub all_children_resized: Rc<LazyEventSource>,
|
||||||
pub tab_bar: RefCell<Option<TabBar>>,
|
pub tab_bar: RefCell<Option<TabBar>>,
|
||||||
|
scroll: Scroller,
|
||||||
pub update_tab_textures_scheduled: Cell<bool>,
|
pub update_tab_textures_scheduled: Cell<bool>,
|
||||||
pub ephemeral: Cell<Ephemeral>,
|
pub ephemeral: Cell<Ephemeral>,
|
||||||
}
|
}
|
||||||
|
|
@ -253,6 +255,7 @@ impl ContainerNode {
|
||||||
child_removed: state.lazy_event_sources.create_source(),
|
child_removed: state.lazy_event_sources.create_source(),
|
||||||
all_children_resized: state.post_layout_event_sources.create_source(),
|
all_children_resized: state.post_layout_event_sources.create_source(),
|
||||||
tab_bar: RefCell::new(None),
|
tab_bar: RefCell::new(None),
|
||||||
|
scroll: Default::default(),
|
||||||
update_tab_textures_scheduled: Cell::new(false),
|
update_tab_textures_scheduled: Cell::new(false),
|
||||||
ephemeral: Cell::new(Ephemeral::Off),
|
ephemeral: Cell::new(Ephemeral::Off),
|
||||||
});
|
});
|
||||||
|
|
@ -277,6 +280,47 @@ impl ContainerNode {
|
||||||
self.add_child_x(prev, new, |prev, new| self.add_child_after_(prev, new));
|
self.add_child_x(prev, new, |prev, new| self.add_child_after_(prev, new));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_tiled_child_after(self: &Rc<Self>, prev: &dyn Node, new: Rc<dyn ToplevelNode>) {
|
||||||
|
if !self.state.theme.autotile_enabled.get()
|
||||||
|
|| self.mono_child.is_some()
|
||||||
|
|| self.num_children.get() <= 1
|
||||||
|
{
|
||||||
|
self.add_child_after(prev, new);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let focused = self
|
||||||
|
.child_nodes
|
||||||
|
.borrow()
|
||||||
|
.get(&prev.node_id())
|
||||||
|
.map(|n| n.to_ref());
|
||||||
|
let Some(focused) = focused else {
|
||||||
|
log::error!(
|
||||||
|
"Tried to autotile a child into a container but the preceding node is not in the container"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let focused_node = focused.node.clone();
|
||||||
|
let focused_active = focused_node.tl_data().active();
|
||||||
|
let sub = ContainerNode::new(
|
||||||
|
&self.state,
|
||||||
|
&self.workspace.get(),
|
||||||
|
focused_node.clone(),
|
||||||
|
self.split.get().other(),
|
||||||
|
);
|
||||||
|
// Autotile-created groups are structural and collapse once only one
|
||||||
|
// child remains. Explicit make-group commands control their own
|
||||||
|
// grouping through the regular manual paths.
|
||||||
|
sub.ephemeral.set(Ephemeral::On);
|
||||||
|
sub.append_child(new);
|
||||||
|
let sub_id = sub.node_id();
|
||||||
|
self.clone().cnode_replace_child(&*focused_node, sub);
|
||||||
|
if focused_active
|
||||||
|
&& let Some(group) = self.child_nodes.borrow().get(&sub_id).map(|n| n.to_ref())
|
||||||
|
{
|
||||||
|
self.update_child_active(&group, true, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_child_before(self: &Rc<Self>, prev: &dyn Node, new: Rc<dyn ToplevelNode>) {
|
pub fn add_child_before(self: &Rc<Self>, prev: &dyn Node, new: Rc<dyn ToplevelNode>) {
|
||||||
self.add_child_x(prev, new, |prev, new| self.add_child_before_(prev, new));
|
self.add_child_x(prev, new, |prev, new| self.add_child_before_(prev, new));
|
||||||
}
|
}
|
||||||
|
|
@ -527,6 +571,18 @@ impl ContainerNode {
|
||||||
self.activate_child2(child, false);
|
self.activate_child2(child, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn activate_child_from_input(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
child: &NodeRef<ContainerChild>,
|
||||||
|
seat: &Rc<WlSeatGlobal>,
|
||||||
|
) {
|
||||||
|
self.activate_child(child);
|
||||||
|
child
|
||||||
|
.node
|
||||||
|
.clone()
|
||||||
|
.node_do_focus(seat, Direction::Unspecified);
|
||||||
|
}
|
||||||
|
|
||||||
fn activate_child2(self: &Rc<Self>, child: &NodeRef<ContainerChild>, preserve_focus: bool) {
|
fn activate_child2(self: &Rc<Self>, child: &NodeRef<ContainerChild>, preserve_focus: bool) {
|
||||||
if let Some(mc) = self.mono_child.get() {
|
if let Some(mc) = self.mono_child.get() {
|
||||||
if mc.node.node_id() == child.node.node_id() {
|
if mc.node.node_id() == child.node.node_id() {
|
||||||
|
|
@ -1144,42 +1200,6 @@ impl ContainerNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_child(self: &Rc<Self>, node: Rc<dyn ToplevelNode>, direction: Direction) {
|
pub fn insert_child(self: &Rc<Self>, node: Rc<dyn ToplevelNode>, direction: Direction) {
|
||||||
// Autotile: if the container would become too narrow/tall, wrap the
|
|
||||||
// focused child and new node in a perpendicular sub-container.
|
|
||||||
if self.state.theme.autotile_enabled.get() && self.mono_child.is_none() {
|
|
||||||
let (pw, ph) = self.predict_child_body_size();
|
|
||||||
let opposite = match self.split.get() {
|
|
||||||
ContainerSplit::Horizontal if pw > 0 && ph > 0 && pw < ph => {
|
|
||||||
Some(ContainerSplit::Vertical)
|
|
||||||
}
|
|
||||||
ContainerSplit::Vertical if pw > 0 && ph > 0 && ph < pw => {
|
|
||||||
Some(ContainerSplit::Horizontal)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
if let Some(opp_split) = opposite {
|
|
||||||
if let Some(focused) = self.focus_history.last() {
|
|
||||||
if self.num_children.get() <= 1 {
|
|
||||||
// Single child, autotile not applicable.
|
|
||||||
} else {
|
|
||||||
let focused_node = focused.node.clone();
|
|
||||||
let was_ephemeral = self.ephemeral.replace(Ephemeral::Off);
|
|
||||||
self.clone().cnode_remove_child2(&*focused_node, true);
|
|
||||||
self.ephemeral.set(was_ephemeral);
|
|
||||||
let sub = ContainerNode::new(
|
|
||||||
&self.state,
|
|
||||||
&self.workspace.get(),
|
|
||||||
focused_node,
|
|
||||||
opp_split,
|
|
||||||
);
|
|
||||||
sub.ephemeral.set(Ephemeral::On);
|
|
||||||
sub.append_child(node);
|
|
||||||
self.append_child(sub);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let (split, right) = direction_to_split(direction);
|
let (split, right) = direction_to_split(direction);
|
||||||
if split != self.split.get() || right {
|
if split != self.split.get() || right {
|
||||||
self.append_child(node);
|
self.append_child(node);
|
||||||
|
|
@ -1289,7 +1309,7 @@ impl ContainerNode {
|
||||||
fn button(
|
fn button(
|
||||||
self: Rc<Self>,
|
self: Rc<Self>,
|
||||||
id: CursorType,
|
id: CursorType,
|
||||||
_seat: &Rc<WlSeatGlobal>,
|
seat: &Rc<WlSeatGlobal>,
|
||||||
_time_usec: u64,
|
_time_usec: u64,
|
||||||
pressed: bool,
|
pressed: bool,
|
||||||
button: u32,
|
button: u32,
|
||||||
|
|
@ -1319,7 +1339,7 @@ impl ContainerNode {
|
||||||
if let Some(child) = children.get(&child_id) {
|
if let Some(child) = children.get(&child_id) {
|
||||||
let child_ref = child.to_ref();
|
let child_ref = child.to_ref();
|
||||||
drop(children);
|
drop(children);
|
||||||
self.activate_child(&child_ref);
|
self.activate_child_from_input(&child_ref, seat);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1692,31 +1712,33 @@ impl Node for ContainerNode {
|
||||||
self.button(id, seat, time_usec, state == ButtonState::Pressed, button);
|
self.button(id, seat, time_usec, state == ButtonState::Pressed, button);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn node_on_axis_event(self: Rc<Self>, _seat: &Rc<WlSeatGlobal>, event: &PendingScroll) {
|
fn node_on_axis_event(self: Rc<Self>, seat: &Rc<WlSeatGlobal>, event: &PendingScroll) {
|
||||||
if self.mono_child.is_none() {
|
if self.mono_child.is_none() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Use vertical scroll (index 1) to switch tabs.
|
let steps = match self.scroll.handle(event) {
|
||||||
let v = match event.v120[1].get() {
|
Some(steps) => steps,
|
||||||
Some(v) if v != 0 => v,
|
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
let mono = match self.mono_child.get() {
|
let mut target = match self.mono_child.get() {
|
||||||
Some(m) => m,
|
Some(m) => m,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
let next = if v > 0 {
|
let current_id = target.node.node_id();
|
||||||
// Scroll down → next tab.
|
for _ in 0..steps.abs() {
|
||||||
mono.next().or_else(|| self.children.first())
|
let next = if steps > 0 {
|
||||||
} else {
|
target.next().or_else(|| self.children.first())
|
||||||
// Scroll up → previous tab.
|
} else {
|
||||||
mono.prev().or_else(|| self.children.last())
|
target.prev().or_else(|| self.children.last())
|
||||||
};
|
};
|
||||||
if let Some(next) = next {
|
match next {
|
||||||
if next.node.node_id() != mono.node.node_id() {
|
Some(next) => target = next,
|
||||||
self.activate_child(&next);
|
None => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if target.node.node_id() != current_id {
|
||||||
|
self.activate_child_from_input(&target, seat);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn node_on_leave(&self, seat: &WlSeatGlobal) {
|
fn node_on_leave(&self, seat: &WlSeatGlobal) {
|
||||||
|
|
|
||||||
|
|
@ -154,4 +154,3 @@ impl ContainerNode {
|
||||||
self.damage();
|
self.damage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,25 @@ use {
|
||||||
renderer::Renderer,
|
renderer::Renderer,
|
||||||
state::State,
|
state::State,
|
||||||
tree::{
|
tree::{
|
||||||
FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, NodeLocation,
|
Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink,
|
||||||
OutputNode, StackedNode, TileDragDestination, WorkspaceDragDestination,
|
NodeLocation, OutputNode, StackedNode, TileDragDestination, WorkspaceDragDestination,
|
||||||
WorkspaceNodeId, walker::NodeVisitor,
|
WorkspaceNodeId, walker::NodeVisitor,
|
||||||
},
|
},
|
||||||
utils::{copyhashmap::CopyHashMap, linkedlist::LinkedList},
|
utils::{copyhashmap::CopyHashMap, linkedlist::LinkedList},
|
||||||
},
|
},
|
||||||
std::{cell::Cell, ops::Deref, rc::Rc},
|
std::{
|
||||||
|
cell::{Cell, RefCell},
|
||||||
|
mem,
|
||||||
|
ops::Deref,
|
||||||
|
rc::{Rc, Weak},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct DisplayNode {
|
pub struct DisplayNode {
|
||||||
pub id: NodeId,
|
pub id: NodeId,
|
||||||
pub extents: Cell<Rect>,
|
pub extents: Cell<Rect>,
|
||||||
|
visible: Cell<bool>,
|
||||||
|
suspend_restore_kb_foci: RefCell<Vec<(Rc<WlSeatGlobal>, Weak<dyn Node>)>>,
|
||||||
pub outputs: CopyHashMap<ConnectorId, Rc<OutputNode>>,
|
pub outputs: CopyHashMap<ConnectorId, Rc<OutputNode>>,
|
||||||
pub stacked: Rc<LinkedList<Rc<dyn StackedNode>>>,
|
pub stacked: Rc<LinkedList<Rc<dyn StackedNode>>>,
|
||||||
pub stacked_above_layers: Rc<LinkedList<Rc<dyn StackedNode>>>,
|
pub stacked_above_layers: Rc<LinkedList<Rc<dyn StackedNode>>>,
|
||||||
|
|
@ -31,6 +38,8 @@ impl DisplayNode {
|
||||||
let slf = Self {
|
let slf = Self {
|
||||||
id,
|
id,
|
||||||
extents: Default::default(),
|
extents: Default::default(),
|
||||||
|
visible: Default::default(),
|
||||||
|
suspend_restore_kb_foci: Default::default(),
|
||||||
outputs: Default::default(),
|
outputs: Default::default(),
|
||||||
stacked: Default::default(),
|
stacked: Default::default(),
|
||||||
stacked_above_layers: Default::default(),
|
stacked_above_layers: Default::default(),
|
||||||
|
|
@ -71,6 +80,17 @@ impl DisplayNode {
|
||||||
|
|
||||||
pub fn update_visible(&self, state: &State) {
|
pub fn update_visible(&self, state: &State) {
|
||||||
let visible = state.root_visible();
|
let visible = state.root_visible();
|
||||||
|
let was_visible = self.visible.replace(visible);
|
||||||
|
if !visible && was_visible {
|
||||||
|
let mut foci = self.suspend_restore_kb_foci.borrow_mut();
|
||||||
|
foci.clear();
|
||||||
|
for seat in state.globals.seats.lock().values() {
|
||||||
|
let node = seat.get_keyboard_node();
|
||||||
|
if node.node_id() != self.id {
|
||||||
|
foci.push((seat.clone(), Rc::downgrade(&node)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for output in self.outputs.lock().values() {
|
for output in self.outputs.lock().values() {
|
||||||
output.update_visible();
|
output.update_visible();
|
||||||
}
|
}
|
||||||
|
|
@ -82,6 +102,20 @@ impl DisplayNode {
|
||||||
for seat in state.globals.seats.lock().values() {
|
for seat in state.globals.seats.lock().values() {
|
||||||
seat.set_visible(visible);
|
seat.set_visible(visible);
|
||||||
}
|
}
|
||||||
|
if visible && !was_visible {
|
||||||
|
for (seat, node) in mem::take(&mut *self.suspend_restore_kb_foci.borrow_mut()) {
|
||||||
|
if seat.get_keyboard_node().node_id() == self.id {
|
||||||
|
if let Some(node) = node.upgrade()
|
||||||
|
&& node.node_visible()
|
||||||
|
{
|
||||||
|
seat.focus_node(node);
|
||||||
|
} else {
|
||||||
|
seat.get_fallback_output()
|
||||||
|
.take_keyboard_navigation_focus(&seat, Direction::Unspecified);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if visible {
|
if visible {
|
||||||
state.damage(self.extents.get());
|
state.damage(self.extents.get());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -967,7 +967,7 @@ impl ToplevelData {
|
||||||
}
|
}
|
||||||
fd.workspace.remove_fullscreen_node();
|
fd.workspace.remove_fullscreen_node();
|
||||||
if fd.placeholder.is_destroyed() {
|
if fd.placeholder.is_destroyed() {
|
||||||
state.map_tiled(node);
|
state.map_tiled_without_autotile(node);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let parent = fd.placeholder.tl_data().parent.take().unwrap();
|
let parent = fd.placeholder.tl_data().parent.take().unwrap();
|
||||||
|
|
@ -1247,7 +1247,7 @@ pub fn toplevel_set_floating(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, floati
|
||||||
};
|
};
|
||||||
if !floating {
|
if !floating {
|
||||||
parent.cnode_remove_child2(&*tl, true);
|
parent.cnode_remove_child2(&*tl, true);
|
||||||
state.map_tiled(tl);
|
state.map_tiled_without_autotile(tl);
|
||||||
} else if let Some(ws) = data.workspace.get() {
|
} else if let Some(ws) = data.workspace.get() {
|
||||||
let node_id = data.node_id;
|
let node_id = data.node_id;
|
||||||
let old_body =
|
let old_body =
|
||||||
|
|
@ -1308,3 +1308,54 @@ pub fn toplevel_set_workspace(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, ws: &
|
||||||
tl.tl_set_fullscreen(true, Some(ws.clone()));
|
tl.tl_set_fullscreen(true, Some(ws.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes a toplevel from the tree so it can be parked in a scratchpad.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the window was hidden. A placeholder, a window without a
|
||||||
|
/// parent, or a window that refuses to leave fullscreen cannot be parked.
|
||||||
|
pub fn toplevel_hide_for_scratchpad(tl: Rc<dyn ToplevelNode>) -> bool {
|
||||||
|
if tl.node_is_placeholder() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let data = tl.tl_data();
|
||||||
|
let workspace = data.workspace.get();
|
||||||
|
if data.is_fullscreen.get() {
|
||||||
|
tl.clone().tl_set_fullscreen(false, None);
|
||||||
|
if data.is_fullscreen.get() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(parent) = data.parent.get() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let kb_foci = collect_kb_foci(tl.clone());
|
||||||
|
parent.cnode_remove_child2(&*tl, true);
|
||||||
|
data.parent.take();
|
||||||
|
data.float.take();
|
||||||
|
if data.parent_is_float.replace(false) {
|
||||||
|
data.property_changed(TL_CHANGED_FLOATING);
|
||||||
|
}
|
||||||
|
if data.workspace.take().is_some() {
|
||||||
|
data.property_changed(TL_CHANGED_WORKSPACE);
|
||||||
|
}
|
||||||
|
tl.tl_set_visible(false);
|
||||||
|
if let Some(workspace) = &workspace {
|
||||||
|
for seat in kb_foci {
|
||||||
|
workspace
|
||||||
|
.clone()
|
||||||
|
.node_do_focus(&seat, Direction::Unspecified);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maps a parked scratchpad window back onto `ws`. Scratchpad windows always
|
||||||
|
/// return floating, regardless of how they were laid out before parking.
|
||||||
|
pub fn toplevel_restore_from_scratchpad(
|
||||||
|
state: &Rc<State>,
|
||||||
|
tl: Rc<dyn ToplevelNode>,
|
||||||
|
ws: &Rc<WorkspaceNode>,
|
||||||
|
) {
|
||||||
|
let (width, height) = tl.tl_data().float_size(ws);
|
||||||
|
state.map_floating(tl.clone(), width, height, ws, None);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue