addition
This commit is contained in:
58
src-tauri/Cargo.lock
generated
58
src-tauri/Cargo.lock
generated
@@ -84,10 +84,12 @@ dependencies = [
|
||||
"portable-pty",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shell-words",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-log",
|
||||
"uuid",
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -860,6 +862,16 @@ dependencies = [
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
@@ -1161,6 +1173,16 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
@@ -1863,6 +1885,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.2"
|
||||
@@ -2819,6 +2847,19 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
@@ -4998,6 +5039,23 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
|
||||
dependencies = [
|
||||
"gethostname",
|
||||
"rustix",
|
||||
"x11rb-protocol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb-protocol"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.2"
|
||||
|
||||
@@ -26,3 +26,5 @@ tauri-plugin-log = "2"
|
||||
portable-pty = "0.8"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
base64 = "0.22"
|
||||
x11rb = "0.13"
|
||||
shell-words = "1.1"
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
mod pty;
|
||||
mod x11mod;
|
||||
|
||||
use pty::{pty_kill, pty_resize, pty_spawn, pty_write, PtyState};
|
||||
use x11mod::{app_close, app_launch, app_set_geometry, app_set_visible, X11State};
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.manage(PtyState::default())
|
||||
.manage(X11State::new())
|
||||
.setup(|app| {
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
@@ -21,6 +24,10 @@ pub fn run() {
|
||||
pty_write,
|
||||
pty_resize,
|
||||
pty_kill,
|
||||
app_launch,
|
||||
app_set_geometry,
|
||||
app_set_visible,
|
||||
app_close,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
370
src-tauri/src/x11mod.rs
Normal file
370
src-tauri/src/x11mod.rs
Normal file
@@ -0,0 +1,370 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
use tauri::{AppHandle, Emitter, State};
|
||||
use x11rb::atom_manager;
|
||||
use x11rb::connection::Connection;
|
||||
use x11rb::protocol::xproto::{
|
||||
AtomEnum, ChangeWindowAttributesAux, ClientMessageEvent, ConfigureWindowAux,
|
||||
ConnectionExt as XprotoConnectionExt, EventMask, PropMode,
|
||||
};
|
||||
use x11rb::protocol::Event;
|
||||
use x11rb::rust_connection::RustConnection;
|
||||
use x11rb::wrapper::ConnectionExt as WrapperConnectionExt;
|
||||
|
||||
atom_manager! {
|
||||
pub Atoms: AtomsCookie {
|
||||
_NET_CLIENT_LIST,
|
||||
_NET_WM_PID,
|
||||
_NET_WM_NAME,
|
||||
_NET_WM_STATE,
|
||||
_NET_WM_STATE_ABOVE,
|
||||
_NET_WM_STATE_SKIP_TASKBAR,
|
||||
_NET_WM_STATE_SKIP_PAGER,
|
||||
_MOTIF_WM_HINTS,
|
||||
WM_PROTOCOLS,
|
||||
WM_DELETE_WINDOW,
|
||||
UTF8_STRING,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct X11Conn {
|
||||
conn: Arc<RustConnection>,
|
||||
atoms: Atoms,
|
||||
root: u32,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppInfo {
|
||||
pub xid: u32,
|
||||
pub pid: u32,
|
||||
pub command: String,
|
||||
}
|
||||
|
||||
pub struct X11State {
|
||||
conn: Mutex<Option<X11Conn>>,
|
||||
windows: Arc<Mutex<HashMap<u32, AppInfo>>>,
|
||||
}
|
||||
|
||||
impl X11State {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
conn: Mutex::new(None),
|
||||
windows: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure(&self, app: &AppHandle) -> Result<X11Conn, String> {
|
||||
let mut guard = self.conn.lock().unwrap();
|
||||
if let Some(c) = guard.as_ref() {
|
||||
return Ok(c.clone());
|
||||
}
|
||||
let (raw, screen_num) = RustConnection::connect(None).map_err(|e| e.to_string())?;
|
||||
let conn = Arc::new(raw);
|
||||
let atoms = Atoms::new(&*conn)
|
||||
.map_err(|e| e.to_string())?
|
||||
.reply()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let root = conn.setup().roots[screen_num].root;
|
||||
|
||||
// Subscribe to destroy notifications for embedded windows.
|
||||
conn.change_window_attributes(
|
||||
root,
|
||||
&ChangeWindowAttributesAux::new().event_mask(EventMask::SUBSTRUCTURE_NOTIFY),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
conn.flush().map_err(|e| e.to_string())?;
|
||||
|
||||
// Background event thread for DestroyNotify.
|
||||
let conn_t = conn.clone();
|
||||
let windows_t = self.windows.clone();
|
||||
let app_t = app.clone();
|
||||
std::thread::spawn(move || event_loop(conn_t, windows_t, app_t));
|
||||
|
||||
let c = X11Conn { conn, atoms, root };
|
||||
*guard = Some(c.clone());
|
||||
Ok(c)
|
||||
}
|
||||
}
|
||||
|
||||
fn event_loop(
|
||||
conn: Arc<RustConnection>,
|
||||
windows: Arc<Mutex<HashMap<u32, AppInfo>>>,
|
||||
app: AppHandle,
|
||||
) {
|
||||
loop {
|
||||
match conn.wait_for_event() {
|
||||
Ok(Event::DestroyNotify(e)) => {
|
||||
let removed = windows.lock().unwrap().remove(&e.window);
|
||||
if removed.is_some() {
|
||||
let _ = app.emit("app:destroyed", e.window);
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LaunchOpts {
|
||||
pub command: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct LaunchedApp {
|
||||
pub xid: u32,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn app_launch(
|
||||
app: AppHandle,
|
||||
state: State<'_, X11State>,
|
||||
opts: LaunchOpts,
|
||||
) -> Result<LaunchedApp, String> {
|
||||
let c = state.ensure(&app)?;
|
||||
|
||||
let parts = shell_words::split(&opts.command).map_err(|e| e.to_string())?;
|
||||
if parts.is_empty() {
|
||||
return Err("empty command".into());
|
||||
}
|
||||
let (program, args) = parts.split_first().unwrap();
|
||||
let child = std::process::Command::new(program)
|
||||
.args(args)
|
||||
.stdin(std::process::Stdio::null())
|
||||
.spawn()
|
||||
.map_err(|e| format!("spawn failed: {}", e))?;
|
||||
let root_pid = child.id();
|
||||
// We don't wait()—the child lives independently. Tauri will SIGHUP it on exit.
|
||||
std::mem::forget(child);
|
||||
|
||||
let xid = wait_for_window_by_pid(&c, root_pid, Duration::from_secs(20))
|
||||
.ok_or_else(|| "could not find window for launched process".to_string())?;
|
||||
|
||||
set_embedded_window_props(&c, xid)?;
|
||||
|
||||
let title = read_window_title(&c, xid).unwrap_or_else(|| opts.command.clone());
|
||||
|
||||
state.windows.lock().unwrap().insert(
|
||||
xid,
|
||||
AppInfo {
|
||||
xid,
|
||||
pid: root_pid,
|
||||
command: opts.command.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
Ok(LaunchedApp { xid, title })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn app_set_geometry(
|
||||
app: AppHandle,
|
||||
state: State<'_, X11State>,
|
||||
window: tauri::WebviewWindow,
|
||||
xid: u32,
|
||||
x: f64,
|
||||
y: f64,
|
||||
width: f64,
|
||||
height: f64,
|
||||
) -> Result<(), String> {
|
||||
let c = state.ensure(&app)?;
|
||||
let sf = window.scale_factor().map_err(|e| e.to_string())?;
|
||||
let pos = window.inner_position().map_err(|e| e.to_string())?;
|
||||
let root_x = pos.x + (x * sf).round() as i32;
|
||||
let root_y = pos.y + (y * sf).round() as i32;
|
||||
let w = ((width * sf).round() as i32).max(1) as u32;
|
||||
let h = ((height * sf).round() as i32).max(1) as u32;
|
||||
c.conn
|
||||
.configure_window(
|
||||
xid,
|
||||
&ConfigureWindowAux::new()
|
||||
.x(root_x)
|
||||
.y(root_y)
|
||||
.width(w)
|
||||
.height(h)
|
||||
.stack_mode(x11rb::protocol::xproto::StackMode::ABOVE),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
c.conn.flush().map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn app_set_visible(
|
||||
app: AppHandle,
|
||||
state: State<'_, X11State>,
|
||||
xid: u32,
|
||||
visible: bool,
|
||||
) -> Result<(), String> {
|
||||
let c = state.ensure(&app)?;
|
||||
if visible {
|
||||
c.conn.map_window(xid).map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
c.conn.unmap_window(xid).map_err(|e| e.to_string())?;
|
||||
}
|
||||
c.conn.flush().map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn app_close(app: AppHandle, state: State<'_, X11State>, xid: u32) -> Result<(), String> {
|
||||
let c = state.ensure(&app)?;
|
||||
// Send WM_DELETE_WINDOW to politely ask the app to close.
|
||||
let event = ClientMessageEvent::new(
|
||||
32,
|
||||
xid,
|
||||
c.atoms.WM_PROTOCOLS,
|
||||
[c.atoms.WM_DELETE_WINDOW, 0, 0, 0, 0],
|
||||
);
|
||||
let _ = c.conn.send_event(false, xid, EventMask::NO_EVENT, event);
|
||||
c.conn.flush().map_err(|e| e.to_string())?;
|
||||
state.windows.lock().unwrap().remove(&xid);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn wait_for_window_by_pid(c: &X11Conn, root_pid: u32, timeout: Duration) -> Option<u32> {
|
||||
let deadline = Instant::now() + timeout;
|
||||
while Instant::now() < deadline {
|
||||
let pids = collect_descendant_pids(root_pid);
|
||||
if let Some(xid) = find_window_for_pids(c, &pids) {
|
||||
return Some(xid);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(150));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn collect_descendant_pids(root: u32) -> Vec<u32> {
|
||||
// Walk /proc to find all processes whose ancestor chain includes root.
|
||||
let mut parents: HashMap<u32, u32> = HashMap::new();
|
||||
let Ok(entries) = std::fs::read_dir("/proc") else {
|
||||
return vec![root];
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name();
|
||||
let Some(name_str) = name.to_str() else {
|
||||
continue;
|
||||
};
|
||||
let Ok(pid) = name_str.parse::<u32>() else {
|
||||
continue;
|
||||
};
|
||||
let stat_path = entry.path().join("stat");
|
||||
let Ok(stat) = std::fs::read_to_string(&stat_path) else {
|
||||
continue;
|
||||
};
|
||||
// Format: "pid (comm) state ppid ..."
|
||||
if let Some(rparen) = stat.rfind(')') {
|
||||
let rest = &stat[rparen + 1..];
|
||||
let fields: Vec<&str> = rest.split_whitespace().collect();
|
||||
if let Some(ppid_str) = fields.get(1) {
|
||||
if let Ok(ppid) = ppid_str.parse::<u32>() {
|
||||
parents.insert(pid, ppid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut result = vec![root];
|
||||
let mut grew = true;
|
||||
while grew {
|
||||
grew = false;
|
||||
for (&pid, &ppid) in &parents {
|
||||
if result.contains(&ppid) && !result.contains(&pid) {
|
||||
result.push(pid);
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn find_window_for_pids(c: &X11Conn, pids: &[u32]) -> Option<u32> {
|
||||
let reply = c
|
||||
.conn
|
||||
.get_property(false, c.root, c.atoms._NET_CLIENT_LIST, AtomEnum::WINDOW, 0, 1024)
|
||||
.ok()?
|
||||
.reply()
|
||||
.ok()?;
|
||||
let windows: Vec<u32> = reply.value32()?.collect();
|
||||
for xid in windows {
|
||||
if let Some(wpid) = read_window_pid(c, xid) {
|
||||
if pids.contains(&wpid) {
|
||||
return Some(xid);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn read_window_pid(c: &X11Conn, xid: u32) -> Option<u32> {
|
||||
let reply = c
|
||||
.conn
|
||||
.get_property(false, xid, c.atoms._NET_WM_PID, AtomEnum::CARDINAL, 0, 1)
|
||||
.ok()?
|
||||
.reply()
|
||||
.ok()?;
|
||||
reply.value32().and_then(|mut it| it.next())
|
||||
}
|
||||
|
||||
fn read_window_title(c: &X11Conn, xid: u32) -> Option<String> {
|
||||
let reply = c
|
||||
.conn
|
||||
.get_property(false, xid, c.atoms._NET_WM_NAME, c.atoms.UTF8_STRING, 0, 1024)
|
||||
.ok()?
|
||||
.reply()
|
||||
.ok()?;
|
||||
let s = String::from_utf8_lossy(&reply.value).to_string();
|
||||
if s.is_empty() {
|
||||
// fallback to WM_NAME
|
||||
let r2 = c
|
||||
.conn
|
||||
.get_property(false, xid, AtomEnum::WM_NAME, AtomEnum::STRING, 0, 1024)
|
||||
.ok()?
|
||||
.reply()
|
||||
.ok()?;
|
||||
Some(String::from_utf8_lossy(&r2.value).to_string())
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_embedded_window_props(c: &X11Conn, xid: u32) -> Result<(), String> {
|
||||
// _MOTIF_WM_HINTS — flags=DECORATIONS (1<<1), decorations=0 (none).
|
||||
let motif = [2u32, 0, 0, 0, 0];
|
||||
c.conn
|
||||
.change_property32(
|
||||
PropMode::REPLACE,
|
||||
xid,
|
||||
c.atoms._MOTIF_WM_HINTS,
|
||||
c.atoms._MOTIF_WM_HINTS,
|
||||
&motif,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// _NET_WM_STATE_ADD: ABOVE + SKIP_TASKBAR + SKIP_PAGER.
|
||||
for state_atom in [
|
||||
c.atoms._NET_WM_STATE_ABOVE,
|
||||
c.atoms._NET_WM_STATE_SKIP_TASKBAR,
|
||||
c.atoms._NET_WM_STATE_SKIP_PAGER,
|
||||
] {
|
||||
let event = ClientMessageEvent::new(
|
||||
32,
|
||||
xid,
|
||||
c.atoms._NET_WM_STATE,
|
||||
[1, state_atom, 0, 0, 0], // _NET_WM_STATE_ADD = 1
|
||||
);
|
||||
let _ = c.conn.send_event(
|
||||
false,
|
||||
c.root,
|
||||
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
||||
event,
|
||||
);
|
||||
}
|
||||
|
||||
c.conn.flush().map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user