Added Markdown preview

This commit is contained in:
Haapy
2026-05-15 11:16:45 +00:00
parent 8bc19de599
commit 595666e94b
265 changed files with 114443 additions and 1017 deletions

60
src-tauri/Cargo.lock generated
View File

@@ -80,18 +80,14 @@ name = "app"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"gdkx11",
"gtk",
"log",
"portable-pty",
"serde",
"serde_json",
"shell-words",
"tauri",
"tauri-build",
"tauri-plugin-log",
"uuid",
"x11rb",
]
[[package]]
@@ -864,16 +860,6 @@ 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"
@@ -1175,16 +1161,6 @@ 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"
@@ -1887,12 +1863,6 @@ 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"
@@ -2849,19 +2819,6 @@ 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"
@@ -5041,23 +4998,6 @@ 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"

View File

@@ -26,9 +26,3 @@ tauri-plugin-log = "2"
portable-pty = "0.8"
uuid = { version = "1", features = ["v4"] }
base64 = "0.22"
x11rb = "0.13"
shell-words = "1.1"
[target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.18"
gdkx11 = "0.18"

View File

@@ -1,23 +1,13 @@
mod pty;
mod x11mod;
use pty::{pty_kill, pty_resize, pty_spawn, pty_write, PtyState};
use x11mod::{
app_close, app_launch, app_set_all_visible, app_set_geometry, app_set_visible, X11State,
};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
// X11 reparenting requires our own window to be an X11 window. On Wayland
// sessions, GTK defaults to the Wayland backend and embedding is impossible.
// Force the X11 backend (via XWayland on Wayland sessions) so we always get
// an X11 toplevel we can reparent into.
// webkit2gtk's hardware compositor fails in VMs / on systems without a
// real GPU and the window goes grey. Fall back to software rendering.
#[cfg(target_os = "linux")]
{
std::env::set_var("GDK_BACKEND", "x11");
// webkit2gtk's hardware compositor fails in VMs / on systems without a
// real GPU and the window goes grey. Falling back to software rendering
// here keeps the app usable everywhere.
if std::env::var_os("WEBKIT_DISABLE_COMPOSITING_MODE").is_none() {
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
}
@@ -28,7 +18,6 @@ pub fn run() {
tauri::Builder::default()
.manage(PtyState::default())
.manage(X11State::new())
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
@@ -44,11 +33,6 @@ pub fn run() {
pty_write,
pty_resize,
pty_kill,
app_launch,
app_set_geometry,
app_set_visible,
app_set_all_visible,
app_close,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@@ -1,474 +0,0 @@
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, StackMode, UnmapNotifyEvent,
UNMAP_NOTIFY_EVENT,
};
use x11rb::protocol::Event;
use x11rb::rust_connection::RustConnection;
use x11rb::wrapper::ConnectionExt as _;
atom_manager! {
pub Atoms: AtomsCookie {
_NET_CLIENT_LIST,
_NET_WM_PID,
_NET_WM_NAME,
_XEMBED,
_XEMBED_INFO,
WM_PROTOCOLS,
WM_DELETE_WINDOW,
UTF8_STRING,
}
}
// XEmbed protocol message opcodes (subset).
const XEMBED_EMBEDDED_NOTIFY: u32 = 0;
#[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>>,
tauri_xid: Mutex<Option<u32>>,
windows: Arc<Mutex<HashMap<u32, AppInfo>>>,
}
impl X11State {
pub fn new() -> Self {
Self {
conn: Mutex::new(None),
tauri_xid: Mutex::new(None),
windows: Arc::new(Mutex::new(HashMap::new())),
}
}
fn ensure_conn(&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;
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 ensure_xid(
&self,
app: &AppHandle,
window: &tauri::WebviewWindow,
) -> Result<u32, String> {
let mut guard = self.tauri_xid.lock().unwrap();
if let Some(xid) = *guard {
return Ok(xid);
}
let xid = fetch_tauri_xid(app, window.clone())?;
*guard = Some(xid);
Ok(xid)
}
}
fn fetch_tauri_xid(app: &AppHandle, window: tauri::WebviewWindow) -> Result<u32, String> {
use std::sync::mpsc;
let (tx, rx) = mpsc::sync_channel(1);
app.run_on_main_thread(move || {
let result: Result<u32, String> = (|| {
use gtk::prelude::*;
let gtk_win = window.gtk_window().map_err(|e| e.to_string())?;
let gdk_win = gtk_win.window().ok_or_else(|| "no GDK window yet".to_string())?;
let x11: gdkx11::X11Window = gdk_win.downcast().map_err(|_| {
"Tauri's window is not an X11 window — likely running on Wayland. \
GDK_BACKEND=x11 should force X11; check that XWayland is installed \
(apt install xwayland) and restart the app."
.to_string()
})?;
Ok(x11.xid() as u32)
})();
let _ = tx.send(result);
})
.map_err(|e| e.to_string())?;
rx.recv().map_err(|e| e.to_string())?
}
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>,
window: tauri::WebviewWindow,
opts: LaunchOpts,
) -> Result<LaunchedApp, String> {
let c = state.ensure_conn(&app)?;
let parent_xid = state.ensure_xid(&app, &window)?;
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();
// Detach: we don't wait() — Tauri exiting will SIGHUP it via the X11 parent.
std::mem::forget(child);
eprintln!(
"[infinite] app_launch: command={:?} pid={} tauri_xid={:#x}",
opts.command, root_pid, parent_xid
);
let client_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())?;
eprintln!("[infinite] found client window {:#x} for pid {}", client_xid, root_pid);
embed_window(&c, client_xid, parent_xid)?;
let title = read_window_title(&c, client_xid).unwrap_or_else(|| opts.command.clone());
state.windows.lock().unwrap().insert(
client_xid,
AppInfo {
xid: client_xid,
pid: root_pid,
command: opts.command.clone(),
},
);
Ok(LaunchedApp { xid: client_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_conn(&app)?;
let sf = window.scale_factor().map_err(|e| e.to_string())?;
let outer = window.outer_position().map_err(|e| e.to_string())?;
let inner = window.inner_position().map_err(|e| e.to_string())?;
let off_x = inner.x - outer.x;
let off_y = inner.y - outer.y;
let phys_x = (x * sf).round() as i32 + off_x;
let phys_y = (y * sf).round() as i32 + off_y;
let phys_w = ((width * sf).round() as i32).max(1) as u32;
let phys_h = ((height * sf).round() as i32).max(1) as u32;
c.conn
.configure_window(
xid,
&ConfigureWindowAux::new()
.x(phys_x)
.y(phys_y)
.width(phys_w)
.height(phys_h)
.stack_mode(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_conn(&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_set_all_visible(
app: AppHandle,
state: State<'_, X11State>,
visible: bool,
) -> Result<(), String> {
let c = state.ensure_conn(&app)?;
let xids: Vec<u32> = state.windows.lock().unwrap().keys().copied().collect();
for xid in xids {
if visible {
let _ = c.conn.map_window(xid);
} else {
let _ = c.conn.unmap_window(xid);
}
}
let _ = c.conn.flush();
Ok(())
}
#[tauri::command]
pub fn app_close(app: AppHandle, state: State<'_, X11State>, xid: u32) -> Result<(), String> {
let c = state.ensure_conn(&app)?;
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 embed_window(c: &X11Conn, client_xid: u32, parent_xid: u32) -> Result<(), String> {
eprintln!(
"[infinite] embed: client={:#x} -> parent={:#x}",
client_xid, parent_xid
);
// XWithdrawWindow protocol: unmap then send synthetic UnmapNotify to root so
// the WM unmanages this window. Without this step Mutter et al. keep treating
// it as a top-level even after reparenting.
let _ = c.conn.unmap_window(client_xid);
let unmap_event = UnmapNotifyEvent {
response_type: UNMAP_NOTIFY_EVENT,
sequence: 0,
event: c.root,
window: client_xid,
from_configure: false,
};
let _ = c.conn.send_event(
false,
c.root,
EventMask::SUBSTRUCTURE_NOTIFY | EventMask::SUBSTRUCTURE_REDIRECT,
unmap_event,
);
let _ = c.conn.sync();
// Listen for destroy / property changes on the embedded window.
c.conn
.change_window_attributes(
client_xid,
&ChangeWindowAttributesAux::new()
.event_mask(EventMask::STRUCTURE_NOTIFY | EventMask::PROPERTY_CHANGE),
)
.map_err(|e| e.to_string())?;
// Reparent into Tauri's X11 toplevel.
c.conn
.reparent_window(client_xid, parent_xid, 0, 0)
.map_err(|e| e.to_string())?;
let _ = c.conn.sync();
// Stack above the webview's GDK X11 surface (siblings under Tauri toplevel).
c.conn
.configure_window(
client_xid,
&ConfigureWindowAux::new().stack_mode(StackMode::ABOVE),
)
.map_err(|e| e.to_string())?;
c.conn.map_window(client_xid).map_err(|e| e.to_string())?;
c.conn.flush().map_err(|e| e.to_string())?;
// Inform the client it is now embedded (XEmbed protocol).
let event = ClientMessageEvent::new(
32,
client_xid,
c.atoms._XEMBED,
[x11rb::CURRENT_TIME, XEMBED_EMBEDDED_NOTIFY, 0, parent_xid, 0],
);
let _ = c.conn.send_event(false, client_xid, EventMask::NO_EVENT, event);
c.conn.flush().map_err(|e| e.to_string())?;
// Verify the reparent took effect at the X11 level. If it didn't, the call
// succeeded but something (WM, compositor) reverted it.
let tree = c
.conn
.query_tree(client_xid)
.map_err(|e| e.to_string())?
.reply()
.map_err(|e| e.to_string())?;
eprintln!(
"[infinite] after reparent: parent={:#x} (wanted {:#x}), root={:#x}",
tree.parent, parent_xid, tree.root
);
if tree.parent != parent_xid {
return Err(format!(
"reparent did not stick: window {:#x} parent is {:#x}, not {:#x}. \
Likely a Wayland session — try logging in to 'Ubuntu on Xorg'.",
client_xid, tree.parent, parent_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> {
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;
};
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() {
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)
}
}