Compare commits
No commits in common. "6028987a6eb89facd7a071be347bf22f7d182d0c" and "1ec8d78f7148a0658a67235280e7c63c3928a795" have entirely different histories.
6028987a6e
...
1ec8d78f71
33
CHANGELOG.md
33
CHANGELOG.md
@ -48,36 +48,3 @@
|
|||||||
clippy clean.
|
clippy clean.
|
||||||
- Docs: `docs/MACROS.md` "Smart interact shortcut" section, `docs/NOTES.md`
|
- Docs: `docs/MACROS.md` "Smart interact shortcut" section, `docs/NOTES.md`
|
||||||
callout, `examples/profile.yaml` updated with comments.
|
callout, `examples/profile.yaml` updated with comments.
|
||||||
|
|
||||||
> Note: After this change, `bind: "interact"` is a single Alt+J send (game_binds.interact).
|
|
||||||
> The full chain trigger is now `bind: "smart_interact"`. Examples and tests updated.
|
|
||||||
|
|
||||||
- **Security:** IPC runtime dir is now `chmod 0o700` and the Unix socket is
|
|
||||||
`chmod 0o600` immediately after `UnixListener::bind`. Any local user with
|
|
||||||
read access could speak our IPC protocol (`Command::Type` etc.); this is
|
|
||||||
the cheapest defense. Helpers: `profile::chmod_runtime_dir()` and
|
|
||||||
`profile::chmod_socket(path)`.
|
|
||||||
|
|
||||||
- **GUI:** New-team wizard now pads `profile.characters` to `slots` before
|
|
||||||
stamping `lutris_game`. A wizard with 5 members and an empty character
|
|
||||||
list previously saved a team with zero Launch rows; it now fills the
|
|
||||||
rows with `Character::default()` and applies the Lutris game to each.
|
|
||||||
- **GUI:** `arm_auto_apply` now waits for a client matching
|
|
||||||
`profile.window_match.class` / `.title` regexes before firing
|
|
||||||
`layout-apply`, instead of firing on the first non-empty client list.
|
|
||||||
With no patterns configured it still requires a non-empty class on the
|
|
||||||
matched client, so existing profiles behave the same. This prevents
|
|
||||||
`layout-apply` against the desktop when only a terminal (or any
|
|
||||||
unrelated window) is open.
|
|
||||||
- **Profile:** `Character` derives `Default` (needed for the padding).
|
|
||||||
|
|
||||||
- **Overlay:** `set_margin` arguments corrected to `(top, right, bottom,
|
|
||||||
left)`. The previous `(rect.y, rect.x, 0, 0)` passed `rect.x` as the
|
|
||||||
*right* margin, which is a no-op under TOP+LEFT anchoring — so the
|
|
||||||
overlay's x offset was silently dropped. Now `(y, 0, 0, x)` which
|
|
||||||
pushes the surface down by `y` and right by `x`.
|
|
||||||
- **Overlay:** Left-click on a slot no longer sets `state.exited = true`.
|
|
||||||
Doing so dropped the badge and left `OverlayHub.by_slot` still
|
|
||||||
holding the slot, so the hub refused to respawn it. The compositor's
|
|
||||||
`zwlr_layer_surface::Closed` event is the only path that tears the
|
|
||||||
live thread down — click just sends the swap IPC and returns.
|
|
||||||
|
|||||||
@ -78,11 +78,11 @@ Do not mash. A second interact while they are still pathing is how characters ru
|
|||||||
|
|
||||||
If they stop short, raise `walk_delay_ms` (2500–4000), or use `interact.style: auto` (CTM stays on), or `interact.style: hold`.
|
If they stop short, raise `walk_delay_ms` (2500–4000), or use `interact.style: auto` (CTM stays on), or `interact.style: hold`.
|
||||||
|
|
||||||
## Smart interact shortcut (`bind: smart_interact`)
|
## Smart interact shortcut (`bind: interact`)
|
||||||
|
|
||||||
The example profile's `loot` map (Alt+G) and `interact` map (Alt+I) both use a **smart shortcut**: a single user keypress fires the full chain (CTM on → Interact with Target → sleep `walk_delay_ms` → CTM off). It is what ISBoxer did with a "Mapped Key" — you press one key, the multibox software sends the whole chain.
|
The example profile's `loot` map (Alt+G) and `interact` map (Alt+I) both use a **smart shortcut**: a single user keypress fires the full chain (CTM on → Interact with Target → sleep `walk_delay_ms` → CTM off). It is what ISBoxer did with a "Mapped Key" — you press one key, the multibox software sends the whole chain.
|
||||||
|
|
||||||
In enBoxer the shortcut is `bind: smart_interact` in a step:
|
In enBoxer the shortcut is `bind: interact` in a step:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: loot
|
- name: loot
|
||||||
@ -90,7 +90,7 @@ In enBoxer the shortcut is `bind: smart_interact` in a step:
|
|||||||
steps:
|
steps:
|
||||||
- bind: assist
|
- bind: assist
|
||||||
target: others
|
target: others
|
||||||
- bind: smart_interact # <-- smart shortcut
|
- bind: interact # <-- smart shortcut
|
||||||
target: others
|
target: others
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -40,7 +40,7 @@ Visible-pixel capture: `grim` (default). Overlay: `mpv --wayland-app-id=enboxer-
|
|||||||
|
|
||||||
## Smart interact shortcut
|
## Smart interact shortcut
|
||||||
|
|
||||||
`bind: smart_interact` in any map step expands at compile time into the full chain
|
`bind: interact` in any map step expands at compile time into the full chain
|
||||||
(CTM on → Interact with Target → walk delay → CTM off), driven by
|
(CTM on → Interact with Target → walk delay → CTM off), driven by
|
||||||
`profile.interact` (`style` + `walk_delay_ms`). This is the ISBoxer Mapped
|
`profile.interact` (`style` + `walk_delay_ms`). This is the ISBoxer Mapped
|
||||||
Key analog — one user keypress, four keystrokes dispatched to every captured
|
Key analog — one user keypress, four keystrokes dispatched to every captured
|
||||||
|
|||||||
@ -114,7 +114,7 @@ maps:
|
|||||||
steps:
|
steps:
|
||||||
- bind: assist
|
- bind: assist
|
||||||
target: others
|
target: others
|
||||||
- bind: smart_interact # full chain (use "interact" for a single Alt+J send)
|
- bind: interact
|
||||||
target: others
|
target: others
|
||||||
- name: loot_manual
|
- name: loot_manual
|
||||||
# Same idea as `loot` but composed by hand. Pick this form if you want to
|
# Same idea as `loot` but composed by hand. Pick this form if you want to
|
||||||
@ -127,7 +127,7 @@ maps:
|
|||||||
target: others
|
target: others
|
||||||
- bind: ctm_on
|
- bind: ctm_on
|
||||||
target: others
|
target: others
|
||||||
- bind: interact # single Alt+J send (NOT the smart shortcut)
|
- bind: interact
|
||||||
target: others
|
target: others
|
||||||
- delay_ms: 5000
|
- delay_ms: 5000
|
||||||
- bind: ctm_off
|
- bind: ctm_off
|
||||||
@ -140,7 +140,7 @@ maps:
|
|||||||
steps:
|
steps:
|
||||||
- bind: ctm_on
|
- bind: ctm_on
|
||||||
target: others
|
target: others
|
||||||
- bind: interact # single Alt+J send (NOT the smart shortcut)
|
- bind: interact
|
||||||
target: others
|
target: others
|
||||||
release_steps:
|
release_steps:
|
||||||
- bind: ctm_off
|
- bind: ctm_off
|
||||||
|
|||||||
@ -205,14 +205,11 @@ impl Engine {
|
|||||||
out.push(Action::Sleep(Duration::from_millis(ms)));
|
out.push(Action::Sleep(Duration::from_millis(ms)));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// ISBoxer-style Mapped Key shortcut: `bind: "smart_interact"` fires
|
// ISBoxer-style Mapped Key shortcut: `bind: "interact"` fires the
|
||||||
// the full CTM-toggle + Interact-with-Target + (optional) wait +
|
// full CTM-toggle + Interact-with-Target + (optional) wait + CTM-off
|
||||||
// CTM-off chain driven by profile.interact (style + walk_delay_ms).
|
// chain driven by profile.interact (style + walk_delay_ms).
|
||||||
// The user only pressed the map hotkey; the daemon composes the chain.
|
// The user only pressed the map hotkey; the daemon composes the chain.
|
||||||
// For per-step control, use `bind: "interact"` to send a single
|
if step.bind.as_deref() == Some("interact") {
|
||||||
// Interact-with-Target keystroke (game_binds.interact = Alt+J) and
|
|
||||||
// compose ctm_on / ctm_off / walk_delay_ms yourself.
|
|
||||||
if step.bind.as_deref() == Some("smart_interact") {
|
|
||||||
let slots = self.resolve_targets(&step.target, &map_name)?;
|
let slots = self.resolve_targets(&step.target, &map_name)?;
|
||||||
let interact_key = self
|
let interact_key = self
|
||||||
.profile
|
.profile
|
||||||
@ -355,7 +352,7 @@ mod tests {
|
|||||||
target: "others".into(),
|
target: "others".into(),
|
||||||
},
|
},
|
||||||
Step {
|
Step {
|
||||||
bind: Some("smart_interact".into()),
|
bind: Some("interact".into()),
|
||||||
key: None,
|
key: None,
|
||||||
delay_ms: None,
|
delay_ms: None,
|
||||||
target: "others".into(),
|
target: "others".into(),
|
||||||
@ -508,37 +505,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn interact_simple_sends_only_alt_j() {
|
|
||||||
// After the smart_interact split, `bind: "interact"` is a single
|
|
||||||
// Alt+J send (game_binds.interact = "g"). No CTM toggle, no sleep,
|
|
||||||
// no second send. Use this from manual chains or release_steps.
|
|
||||||
let mut e = sample();
|
|
||||||
let map = crate::profile::Map {
|
|
||||||
name: "interact_simple".into(),
|
|
||||||
hotkey: Hotkey("Alt+U".into()),
|
|
||||||
hold: false,
|
|
||||||
steps: vec![crate::profile::Step {
|
|
||||||
key: None,
|
|
||||||
bind: Some("interact".into()),
|
|
||||||
delay_ms: None,
|
|
||||||
target: "others".into(),
|
|
||||||
}],
|
|
||||||
release_steps: vec![],
|
|
||||||
};
|
|
||||||
e.profile.maps.push(map);
|
|
||||||
let acts = e.fire("Alt+U", Hold::Tap).unwrap();
|
|
||||||
assert_eq!(acts.len(), 1, "exactly one action: {acts:?}");
|
|
||||||
match &acts[0] {
|
|
||||||
Action::Send { key, slots, hold } => {
|
|
||||||
assert_eq!(key, "g");
|
|
||||||
assert_eq!(*hold, Hold::Tap);
|
|
||||||
assert_eq!(slots, &vec![2, 3]);
|
|
||||||
}
|
|
||||||
other => panic!("expected Send, got {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn interact_smart_shortcut_standard_emits_full_sequence() {
|
fn interact_smart_shortcut_standard_emits_full_sequence() {
|
||||||
// bind: "interact" should fire CTM-on, Interact with Target,
|
// bind: "interact" should fire CTM-on, Interact with Target,
|
||||||
@ -552,7 +518,7 @@ mod tests {
|
|||||||
hold: false,
|
hold: false,
|
||||||
steps: vec![crate::profile::Step {
|
steps: vec![crate::profile::Step {
|
||||||
key: None,
|
key: None,
|
||||||
bind: Some("smart_interact".into()),
|
bind: Some("interact".into()),
|
||||||
delay_ms: None,
|
delay_ms: None,
|
||||||
target: "others".into(),
|
target: "others".into(),
|
||||||
}],
|
}],
|
||||||
@ -589,7 +555,7 @@ mod tests {
|
|||||||
hold: false,
|
hold: false,
|
||||||
steps: vec![crate::profile::Step {
|
steps: vec![crate::profile::Step {
|
||||||
key: None,
|
key: None,
|
||||||
bind: Some("smart_interact".into()),
|
bind: Some("interact".into()),
|
||||||
delay_ms: None,
|
delay_ms: None,
|
||||||
target: "others".into(),
|
target: "others".into(),
|
||||||
}],
|
}],
|
||||||
@ -619,7 +585,7 @@ mod tests {
|
|||||||
hold: true,
|
hold: true,
|
||||||
steps: vec![crate::profile::Step {
|
steps: vec![crate::profile::Step {
|
||||||
key: None,
|
key: None,
|
||||||
bind: Some("smart_interact".into()),
|
bind: Some("interact".into()),
|
||||||
delay_ms: None,
|
delay_ms: None,
|
||||||
target: "others".into(),
|
target: "others".into(),
|
||||||
}],
|
}],
|
||||||
|
|||||||
66
src/gui.rs
66
src/gui.rs
@ -3,8 +3,8 @@
|
|||||||
use crate::hotkey::Hotkey;
|
use crate::hotkey::Hotkey;
|
||||||
use crate::macros::print_macros;
|
use crate::macros::print_macros;
|
||||||
use crate::profile::{
|
use crate::profile::{
|
||||||
Character, default_config_path, Group, LayoutPreset, Map, Mode, NormRect, Profile,
|
default_config_path, Group, LayoutPreset, Map, Mode, NormRect, Profile, Repeater, Step,
|
||||||
Repeater, Step, VideoFx, WindowMatch,
|
VideoFx, WindowMatch,
|
||||||
};
|
};
|
||||||
use crate::session;
|
use crate::session;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@ -1438,22 +1438,9 @@ impl App {
|
|||||||
let mut profile = self.profile.clone();
|
let mut profile = self.profile.clone();
|
||||||
profile.name = name.to_string();
|
profile.name = name.to_string();
|
||||||
profile.slots = wiz.members.max(1);
|
profile.slots = wiz.members.max(1);
|
||||||
// Pad to slot count: truncate any extras, but extend with defaults
|
// Drop characters beyond the new member count.
|
||||||
// so a brand-new profile (members > 0, characters == 0) actually
|
profile.characters.truncate(profile.slots as usize);
|
||||||
// gets a Launch row per member.
|
// Apply the Lutris game pick to every character.
|
||||||
profile.characters
|
|
||||||
.resize(profile.slots as usize, Character::default());
|
|
||||||
// Character::default() leaves slot = 0, which collides with the
|
|
||||||
// Launch button lookup (find(|c| c.slot == slot)). Assign slot 1..=N
|
|
||||||
// to any character still at 0, leaving properly-assigned entries
|
|
||||||
// untouched.
|
|
||||||
for (i, ch) in profile.characters.iter_mut().enumerate() {
|
|
||||||
if ch.slot == 0 {
|
|
||||||
ch.slot = (i as u32) + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Apply the Lutris game pick to every character (including the
|
|
||||||
// freshly-padded ones).
|
|
||||||
if let Some(slug_l) = wiz.lutris_slug.clone() {
|
if let Some(slug_l) = wiz.lutris_slug.clone() {
|
||||||
for ch in &mut profile.characters {
|
for ch in &mut profile.characters {
|
||||||
ch.lutris_game = Some(slug_l.clone());
|
ch.lutris_game = Some(slug_l.clone());
|
||||||
@ -1632,42 +1619,12 @@ impl App {
|
|||||||
// and then sends the layout-apply IPC. Detached; logs on error.
|
// and then sends the layout-apply IPC. Detached; logs on error.
|
||||||
let profile_path = self.path.clone();
|
let profile_path = self.path.clone();
|
||||||
let allow = self.allow_layout;
|
let allow = self.allow_layout;
|
||||||
// Compile the profile's window_match patterns once, outside the
|
|
||||||
// poll loop. If a pattern is malformed we treat it as "not
|
|
||||||
// configured" rather than crashing the auto-apply thread.
|
|
||||||
let class_pat = self
|
|
||||||
.profile
|
|
||||||
.window_match
|
|
||||||
.class
|
|
||||||
.as_deref()
|
|
||||||
.and_then(|p| regex::Regex::new(p).ok());
|
|
||||||
let title_pat = self
|
|
||||||
.profile
|
|
||||||
.window_match
|
|
||||||
.title
|
|
||||||
.as_deref()
|
|
||||||
.and_then(|p| regex::Regex::new(p).ok());
|
|
||||||
let any_pattern = class_pat.is_some() || title_pat.is_some();
|
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
.name("enboxer-auto-apply".into())
|
.name("enboxer-auto-apply".into())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
use std::process::Command as SyncCommand;
|
use std::process::Command as SyncCommand;
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let deadline = std::time::Duration::from_secs(30);
|
let deadline = std::time::Duration::from_secs(30);
|
||||||
// Returns true if the client's class/title match the
|
|
||||||
// configured patterns. If neither pattern is configured
|
|
||||||
// (no `window_match` set on the profile), accept the
|
|
||||||
// first client with a non-empty class so existing
|
|
||||||
// profiles keep working.
|
|
||||||
let matched = |cls: &str, ttl: &str| -> bool {
|
|
||||||
let class_ok = class_pat.as_ref().is_none_or(|re| re.is_match(cls));
|
|
||||||
let title_ok = title_pat.as_ref().is_none_or(|re| re.is_match(ttl));
|
|
||||||
if !any_pattern {
|
|
||||||
!cls.is_empty()
|
|
||||||
} else {
|
|
||||||
class_ok && title_ok
|
|
||||||
}
|
|
||||||
};
|
|
||||||
while start.elapsed() < deadline {
|
while start.elapsed() < deadline {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||||
let out = SyncCommand::new("hyprctl")
|
let out = SyncCommand::new("hyprctl")
|
||||||
@ -1678,18 +1635,7 @@ impl App {
|
|||||||
serde_json::from_slice::<serde_json::Value>(&out.stdout)
|
serde_json::from_slice::<serde_json::Value>(&out.stdout)
|
||||||
{
|
{
|
||||||
if let Some(arr) = v.as_array() {
|
if let Some(arr) = v.as_array() {
|
||||||
let hit = arr.iter().any(|c| {
|
if !arr.is_empty() {
|
||||||
let cls = c
|
|
||||||
.get("class")
|
|
||||||
.and_then(|x| x.as_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
let ttl = c
|
|
||||||
.get("title")
|
|
||||||
.and_then(|x| x.as_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
matched(cls, ttl)
|
|
||||||
});
|
|
||||||
if hit {
|
|
||||||
let mut cmd = SyncCommand::new(
|
let mut cmd = SyncCommand::new(
|
||||||
std::env::current_exe()
|
std::env::current_exe()
|
||||||
.unwrap_or_else(|_| std::path::PathBuf::from("enboxer")),
|
.unwrap_or_else(|_| std::path::PathBuf::from("enboxer")),
|
||||||
|
|||||||
24
src/hypr.rs
24
src/hypr.rs
@ -227,15 +227,12 @@ fn shell_single(s: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Hyprland exec_cmd string. `args` is the IPC line after the binary, e.g. `hotkey Alt+G`.
|
/// Hyprland exec_cmd string. `args` is the IPC line after the binary, e.g. `hotkey Alt+G`.
|
||||||
/// `bin`, `sock`, and `args` are all single-quoted so a map hotkey containing
|
|
||||||
/// shell metacharacters (`;`, backticks, `$()`, quotes, spaces) cannot inject
|
|
||||||
/// into the compositor's `exec_cmd`.
|
|
||||||
pub fn bind_command(bin: &str, sock: &str, args: &str) -> String {
|
pub fn bind_command(bin: &str, sock: &str, args: &str) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{} ipc --sock {} {}",
|
"{} ipc --sock {} {}",
|
||||||
shell_single(bin),
|
shell_single(bin),
|
||||||
shell_single(sock),
|
shell_single(sock),
|
||||||
shell_single(args)
|
args
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,30 +312,15 @@ where
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::bind_command;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bind_command_has_one_ipc() {
|
fn bind_command_has_one_ipc() {
|
||||||
let cmd = bind_command("/tmp/enboxer", "/tmp/enboxer.sock", "hotkey Alt+G");
|
let cmd = bind_command("/tmp/enboxer", "/tmp/enboxer.sock", "hotkey Alt+G");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cmd,
|
cmd,
|
||||||
"'/tmp/enboxer' ipc --sock '/tmp/enboxer.sock' 'hotkey Alt+G'"
|
"'/tmp/enboxer' ipc --sock '/tmp/enboxer.sock' hotkey Alt+G"
|
||||||
);
|
);
|
||||||
assert_eq!(cmd.matches(" ipc ").count(), 1);
|
assert_eq!(cmd.matches(" ipc ").count(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn bind_command_quotes_args_with_metacharacters() {
|
|
||||||
// shell_single must escape a hostile arg (semicolon, backtick,
|
|
||||||
// dollar-paren) so it cannot inject into Hyprland's exec_cmd.
|
|
||||||
let s = bind_command(
|
|
||||||
"enboxer",
|
|
||||||
"/tmp/enboxer.sock",
|
|
||||||
"hotkey Alt+G; cat /etc/passwd",
|
|
||||||
);
|
|
||||||
assert!(s.starts_with("'enboxer' ipc --sock '/tmp/enboxer.sock' "));
|
|
||||||
assert!(s.contains("'hotkey Alt+G; cat /etc/passwd'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -157,7 +157,7 @@ pub enum InteractStyle {
|
|||||||
Hold,
|
Hold,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Character {
|
pub struct Character {
|
||||||
pub slot: u32,
|
pub slot: u32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@ -462,38 +462,6 @@ pub fn runtime_dir() -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `chmod 0o700` the runtime dir so only this user can read/write.
|
|
||||||
///
|
|
||||||
/// Any local user with read access to the socket could speak our IPC
|
|
||||||
/// protocol (including `Command::Type`, which is wide-open text injection
|
|
||||||
/// into game windows). Permissions are the cheapest defense.
|
|
||||||
pub fn chmod_runtime_dir() {
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
let dir = runtime_dir();
|
|
||||||
let _ = std::fs::set_permissions(
|
|
||||||
&dir,
|
|
||||||
std::fs::Permissions::from_mode(0o700),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `chmod 0o600` the IPC socket so only this user can connect.
|
|
||||||
///
|
|
||||||
/// Belt-and-braces with `chmod_runtime_dir`: the dir alone is not enough
|
|
||||||
/// if other shared paths leaked earlier.
|
|
||||||
pub fn chmod_socket<P: AsRef<std::path::Path>>(path: P) {
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
let _ = std::fs::set_permissions(
|
|
||||||
path.as_ref(),
|
|
||||||
std::fs::Permissions::from_mode(0o600),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::NormRect;
|
use super::NormRect;
|
||||||
@ -522,39 +490,3 @@ mod tests {
|
|||||||
assert_eq!(r.to_pixels(100, 50, 200, 100), (110, 70, 80, 40));
|
assert_eq!(r.to_pixels(100, 50, 200, 100), (110, 70, 80, 40));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
#[test]
|
|
||||||
fn chmod_socket_sets_0o600() {
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
let dir = std::env::temp_dir().join("enboxer-test-chmod");
|
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
|
||||||
let sock = dir.join("test.sock");
|
|
||||||
std::fs::write(&sock, b"").unwrap();
|
|
||||||
std::fs::set_permissions(&sock, std::fs::Permissions::from_mode(0o644)).unwrap();
|
|
||||||
chmod_socket(&sock);
|
|
||||||
let m = std::fs::metadata(&sock).unwrap().permissions().mode() & 0o777;
|
|
||||||
assert_eq!(m, 0o600, "expected 0o600, got {m:o}");
|
|
||||||
std::fs::remove_file(&sock).ok();
|
|
||||||
std::fs::remove_dir(&dir).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
#[test]
|
|
||||||
fn chmod_runtime_dir_sets_0o700() {
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
// Save and restore the real dir perms around the test so we don't break
|
|
||||||
// the live session if it happens to share XDG_RUNTIME_DIR.
|
|
||||||
let dir = runtime_dir();
|
|
||||||
let _ = std::fs::create_dir_all(&dir);
|
|
||||||
let saved = std::fs::metadata(&dir).ok().map(|m| m.permissions().mode() & 0o777);
|
|
||||||
// Force 0o755 so the helper actually has to change it.
|
|
||||||
let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755));
|
|
||||||
chmod_runtime_dir();
|
|
||||||
let m = std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777;
|
|
||||||
assert_eq!(m, 0o700, "expected 0o700, got {m:o}");
|
|
||||||
if let Some(s) = saved {
|
|
||||||
let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(s));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@ -43,7 +43,6 @@ impl Session {
|
|||||||
|
|
||||||
pub async fn run(profile: Profile, sock: PathBuf) -> Result<()> {
|
pub async fn run(profile: Profile, sock: PathBuf) -> Result<()> {
|
||||||
std::fs::create_dir_all(runtime_dir()).ok();
|
std::fs::create_dir_all(runtime_dir()).ok();
|
||||||
crate::profile::chmod_runtime_dir();
|
|
||||||
if sock.exists() {
|
if sock.exists() {
|
||||||
let _ = std::fs::remove_file(&sock);
|
let _ = std::fs::remove_file(&sock);
|
||||||
}
|
}
|
||||||
@ -70,7 +69,6 @@ pub async fn run(profile: Profile, sock: PathBuf) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let listener = UnixListener::bind(&sock).with_context(|| format!("bind {}", sock.display()))?;
|
let listener = UnixListener::bind(&sock).with_context(|| format!("bind {}", sock.display()))?;
|
||||||
crate::profile::chmod_socket(&sock);
|
|
||||||
tracing::info!("ipc {}", sock.display());
|
tracing::info!("ipc {}", sock.display());
|
||||||
|
|
||||||
let s1 = session.clone();
|
let s1 = session.clone();
|
||||||
|
|||||||
@ -229,12 +229,9 @@ fn run(slot: u32, rect: OverlayRect, ipc_sock: PathBuf) -> anyhow::Result<()> {
|
|||||||
layer.set_anchor(zwlr_layer_surface_v1::Anchor::Top | zwlr_layer_surface_v1::Anchor::Left);
|
layer.set_anchor(zwlr_layer_surface_v1::Anchor::Top | zwlr_layer_surface_v1::Anchor::Left);
|
||||||
layer.set_size(rect.w.max(1) as u32, rect.h.max(1) as u32);
|
layer.set_size(rect.w.max(1) as u32, rect.h.max(1) as u32);
|
||||||
layer.set_exclusive_zone(-1);
|
layer.set_exclusive_zone(-1);
|
||||||
// zwlr_layer_surface::set_margin is (top, right, bottom, left).
|
// Margins encode the offset: with TOP+LEFT anchor, a positive top
|
||||||
// With TOP+LEFT anchor: top margin pushes the surface down, left
|
// margin pushes the surface down; positive left pushes it right.
|
||||||
// margin pushes it right. (Earlier versions of this code passed
|
layer.set_margin(rect.y.max(0), rect.x.max(0), 0, 0);
|
||||||
// rect.x as the right margin, which is a no-op when only TOP+LEFT
|
|
||||||
// are anchored.)
|
|
||||||
layer.set_margin(rect.y.max(0), 0, 0, rect.x.max(0));
|
|
||||||
layer.set_keyboard_interactivity(zwlr_layer_surface_v1::KeyboardInteractivity::None);
|
layer.set_keyboard_interactivity(zwlr_layer_surface_v1::KeyboardInteractivity::None);
|
||||||
state.surface = Some(surface.clone());
|
state.surface = Some(surface.clone());
|
||||||
state.layer_surface = Some(layer);
|
state.layer_surface = Some(layer);
|
||||||
@ -466,12 +463,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for OverlayState {
|
|||||||
&& matches!(btn_state, wayland_client::WEnum::Value(wl_pointer::ButtonState::Pressed))
|
&& matches!(btn_state, wayland_client::WEnum::Value(wl_pointer::ButtonState::Pressed))
|
||||||
{
|
{
|
||||||
send_swap(state.slot, &state.ipc_sock);
|
send_swap(state.slot, &state.ipc_sock);
|
||||||
// NOTE: do not flip `state.exited = true` here. Doing so
|
state.exited = true;
|
||||||
// destroys the badge and `OverlayHub.by_slot` still holds
|
|
||||||
// the slot, so the hub refuses to respawn it (it thinks
|
|
||||||
// the slot is already served). The compositor's
|
|
||||||
// zwlr_layer_surface::Closed event is the only path that
|
|
||||||
// should tear down the live thread.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user