Recover Rime faults, persist window modes and fix GPU allocation
This commit is contained in:
1 parent
8e9a5838ab
commit
6f34fd080f
27 files changed
+1260
-34
No files matched your search
@@ -20,6 +20,7 @@ tiny-skia = "0.11.4"
|
||||
wayland-client.workspace = true
|
||||
wayland-protocols = { version = "0.32.13", features = ["client", "staging"] }
|
||||
wayland-protocols-misc = { version = "0.3.12", features = ["client"] }
|
||||
wayland-protocols-wlr.workspace = true
|
||||
xkbcommon = "0.9.0"
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
//! Integration-test input source. Refuses physical sessions and non-Ferese
|
||||
//! servers before creating a keyboard; never injects into the host desktop.
|
||||
//! servers before creating a keyboard. Physical validation additionally requires
|
||||
//! the private test runtime and the exact disposable compositor's peer PID.
|
||||
use std::error::Error;
|
||||
use std::io::{Seek, Write};
|
||||
use std::os::fd::AsFd;
|
||||
use std::os::fd::{AsFd, AsRawFd};
|
||||
use std::time::{Duration, Instant};
|
||||
use wayland_client::protocol::{wl_output, wl_registry, wl_seat};
|
||||
use wayland_client::protocol::{wl_output, wl_pointer, wl_registry, wl_seat};
|
||||
use wayland_client::{Connection, Dispatch, QueueHandle, delegate_noop};
|
||||
use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{
|
||||
zwp_virtual_keyboard_manager_v1 as manager, zwp_virtual_keyboard_v1 as keyboard,
|
||||
};
|
||||
use wayland_protocols_wlr::virtual_pointer::v1::client::{
|
||||
zwlr_virtual_pointer_manager_v1 as pointer_manager, zwlr_virtual_pointer_v1 as virtual_pointer,
|
||||
};
|
||||
use xkbcommon::xkb;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -17,6 +21,7 @@ struct State {
|
||||
seat: Option<wl_seat::WlSeat>,
|
||||
ferese: bool,
|
||||
outputs: Vec<String>,
|
||||
pointer_manager: Option<pointer_manager::ZwlrVirtualPointerManagerV1>,
|
||||
}
|
||||
impl Dispatch<wl_registry::WlRegistry, ()> for State {
|
||||
fn event(
|
||||
@@ -42,6 +47,9 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
|
||||
"wl_output" if version >= 4 => {
|
||||
let _: wl_output::WlOutput = registry.bind(name, 4, qh, ());
|
||||
}
|
||||
"zwlr_virtual_pointer_manager_v1" => {
|
||||
state.pointer_manager = Some(registry.bind(name, version.min(2), qh, ()));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -65,6 +73,16 @@ delegate_noop!(State: ignore wl_seat::WlSeat);
|
||||
delegate_noop!(State: ignore manager::ZwpVirtualKeyboardManagerV1);
|
||||
delegate_noop!(State: ignore keyboard::ZwpVirtualKeyboardV1);
|
||||
|
||||
fn parse_numbers(text: &str) -> Result<Vec<u32>, Box<dyn Error>> {
|
||||
Ok(text
|
||||
.split(',')
|
||||
.map(str::parse)
|
||||
.collect::<Result<Vec<_>, _>>()?)
|
||||
}
|
||||
|
||||
delegate_noop!(State: ignore pointer_manager::ZwlrVirtualPointerManagerV1);
|
||||
delegate_noop!(State: ignore virtual_pointer::ZwlrVirtualPointerV1);
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
if std::env::var("FERESE_TEST_INPUT").as_deref() != Ok("1") {
|
||||
return Err(
|
||||
@@ -72,9 +90,33 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
);
|
||||
}
|
||||
let mut sequences: Vec<Vec<u32>> = Vec::new();
|
||||
let mut clicks = Vec::new();
|
||||
let mut drag = None;
|
||||
let mut extent = (1280, 800);
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--extent" => {
|
||||
let v = parse_numbers(&args.next().ok_or("missing extent")?)?;
|
||||
if v.len() != 2 || v[0] == 0 || v[1] == 0 {
|
||||
return Err("invalid extent".into());
|
||||
}
|
||||
extent = (v[0], v[1]);
|
||||
}
|
||||
"--click" => {
|
||||
let v = parse_numbers(&args.next().ok_or("missing click position")?)?;
|
||||
if v.len() != 2 {
|
||||
return Err("invalid click".into());
|
||||
}
|
||||
clicks.push((v[0], v[1]));
|
||||
}
|
||||
"--drag" => {
|
||||
let v = parse_numbers(&args.next().ok_or("missing drag positions")?)?;
|
||||
if v.len() != 4 {
|
||||
return Err("invalid drag".into());
|
||||
}
|
||||
drag = Some(v);
|
||||
}
|
||||
"--keys" => {
|
||||
for character in args.next().ok_or("missing text")?.chars() {
|
||||
sequences.push(vec![character as u32]);
|
||||
@@ -101,10 +143,36 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut state = State::default();
|
||||
queue.roundtrip(&mut state)?;
|
||||
queue.roundtrip(&mut state)?;
|
||||
if !state.ferese
|
||||
|| state.outputs.is_empty()
|
||||
|| state.outputs.iter().any(|name| name != "ferese-winit")
|
||||
{
|
||||
let nested = state.outputs.iter().all(|name| name == "ferese-winit");
|
||||
// SAFETY: getuid takes no pointers and has no preconditions.
|
||||
let uid = unsafe { libc::getuid() };
|
||||
let drm = std::env::var("XDG_RUNTIME_DIR")
|
||||
.ok()
|
||||
.is_some_and(|runtime| runtime == format!("/run/user/{uid}/ferese-rime-drm-validation"))
|
||||
&& std::env::var("FERESE_TEST_DRM_PID")
|
||||
.ok()
|
||||
.and_then(|pid| pid.parse::<i32>().ok())
|
||||
.is_some_and(|expected| {
|
||||
let mut credentials = libc::ucred {
|
||||
pid: 0,
|
||||
uid: 0,
|
||||
gid: 0,
|
||||
};
|
||||
let mut size = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
|
||||
// SAFETY: the socket is live and the output/length pointers
|
||||
// refer to correctly sized initialized storage for SO_PEERCRED.
|
||||
unsafe {
|
||||
libc::getsockopt(
|
||||
connection.as_fd().as_raw_fd(),
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_PEERCRED,
|
||||
(&mut credentials as *mut libc::ucred).cast(),
|
||||
&mut size,
|
||||
) == 0
|
||||
&& credentials.pid == expected
|
||||
}
|
||||
});
|
||||
if !state.ferese || state.outputs.is_empty() || !(nested || drm) {
|
||||
return Err("refusing input: target is not an isolated Ferese nested output".into());
|
||||
}
|
||||
let keyboard = state
|
||||
@@ -152,6 +220,83 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
std::thread::sleep(Duration::from_millis(35));
|
||||
}
|
||||
}
|
||||
if !clicks.is_empty() || drag.is_some() {
|
||||
let pointer = state
|
||||
.pointer_manager
|
||||
.as_ref()
|
||||
.ok_or("test pointer is not enabled on this compositor")?
|
||||
.create_virtual_pointer(state.seat.as_ref(), &qh, ());
|
||||
for (x, y) in clicks {
|
||||
pointer.motion_absolute(
|
||||
started.elapsed().as_millis() as u32,
|
||||
x,
|
||||
y,
|
||||
extent.0,
|
||||
extent.1,
|
||||
);
|
||||
pointer.frame();
|
||||
queue.roundtrip(&mut state)?;
|
||||
std::thread::sleep(Duration::from_millis(80));
|
||||
pointer.button(
|
||||
started.elapsed().as_millis() as u32,
|
||||
0x110,
|
||||
wl_pointer::ButtonState::Pressed,
|
||||
);
|
||||
pointer.frame();
|
||||
queue.roundtrip(&mut state)?;
|
||||
std::thread::sleep(Duration::from_millis(80));
|
||||
pointer.button(
|
||||
started.elapsed().as_millis() as u32,
|
||||
0x110,
|
||||
wl_pointer::ButtonState::Released,
|
||||
);
|
||||
pointer.frame();
|
||||
queue.roundtrip(&mut state)?;
|
||||
std::thread::sleep(Duration::from_millis(150));
|
||||
}
|
||||
if let Some(v) = drag {
|
||||
pointer.motion_absolute(
|
||||
started.elapsed().as_millis() as u32,
|
||||
v[0],
|
||||
v[1],
|
||||
extent.0,
|
||||
extent.1,
|
||||
);
|
||||
pointer.frame();
|
||||
keyboard.key(started.elapsed().as_millis() as u32, 125, 1);
|
||||
queue.roundtrip(&mut state)?;
|
||||
pointer.button(
|
||||
started.elapsed().as_millis() as u32,
|
||||
0x110,
|
||||
wl_pointer::ButtonState::Pressed,
|
||||
);
|
||||
pointer.frame();
|
||||
queue.roundtrip(&mut state)?;
|
||||
for step in 1..=20i64 {
|
||||
let x = v[0] as i64 + (v[2] as i64 - v[0] as i64) * step / 20;
|
||||
let y = v[1] as i64 + (v[3] as i64 - v[1] as i64) * step / 20;
|
||||
pointer.motion_absolute(
|
||||
started.elapsed().as_millis() as u32,
|
||||
x as u32,
|
||||
y as u32,
|
||||
extent.0,
|
||||
extent.1,
|
||||
);
|
||||
pointer.frame();
|
||||
queue.roundtrip(&mut state)?;
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
pointer.button(
|
||||
started.elapsed().as_millis() as u32,
|
||||
0x110,
|
||||
wl_pointer::ButtonState::Released,
|
||||
);
|
||||
pointer.frame();
|
||||
keyboard.key(started.elapsed().as_millis() as u32, 125, 0);
|
||||
queue.roundtrip(&mut state)?;
|
||||
}
|
||||
pointer.destroy();
|
||||
}
|
||||
keyboard.destroy();
|
||||
queue.roundtrip(&mut state)?;
|
||||
println!("nested test input complete");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod panel;
|
||||
pub mod profiles;
|
||||
|
||||
/// The number-row paging keys are local to a composition with candidates.
|
||||
/// Control/Alt/Super shortcuts and literal ASCII typing retain their symbols.
|
||||
@@ -31,6 +32,7 @@ pub struct Candidate {
|
||||
pub struct Snapshot {
|
||||
pub accepted: bool,
|
||||
pub ascii: bool,
|
||||
pub schema: String,
|
||||
pub commit: String,
|
||||
pub preedit: String,
|
||||
/// Byte offset, matching text-input-v3 (not a character count).
|
||||
@@ -52,6 +54,7 @@ struct RawResult {
|
||||
count: c_int,
|
||||
commit: *mut c_char,
|
||||
preedit: *mut c_char,
|
||||
schema: *mut c_char,
|
||||
texts: [*mut c_char; 9],
|
||||
comments: [*mut c_char; 9],
|
||||
}
|
||||
@@ -62,6 +65,7 @@ unsafe extern "C" {
|
||||
fn ferese_rime_snapshot(key: c_int, mask: c_int, out: *mut RawResult);
|
||||
fn ferese_rime_result_free(out: *mut RawResult);
|
||||
fn ferese_rime_clear();
|
||||
fn ferese_rime_profile(schema: *const c_char, ascii_mode: c_int) -> c_int;
|
||||
fn ferese_rime_close();
|
||||
}
|
||||
|
||||
@@ -113,6 +117,15 @@ impl Engine {
|
||||
unsafe { ferese_rime_clear() };
|
||||
}
|
||||
|
||||
pub fn set_profile(&mut self, profile: &profiles::Profile) -> Result<(), String> {
|
||||
let schema = CString::new(profile.schema.as_str()).map_err(|_| "invalid schema")?;
|
||||
// SAFETY: the unique session and schema string remain live during this call.
|
||||
if unsafe { ferese_rime_profile(schema.as_ptr(), i32::from(profile.ascii)) } == 0 {
|
||||
return Err(format!("Rime schema is unavailable: {}", profile.schema));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn snapshot(&mut self, key: i32, mask: i32) -> Snapshot {
|
||||
// SAFETY: all-zero is valid for this C struct's integers/null pointers;
|
||||
// the shim initializes and returns owned, NUL-terminated UTF-8 strings.
|
||||
@@ -136,6 +149,7 @@ impl Engine {
|
||||
let result = Snapshot {
|
||||
accepted: raw.accepted != 0,
|
||||
ascii: raw.ascii_mode != 0,
|
||||
schema: text(raw.schema),
|
||||
commit: text(raw.commit),
|
||||
preedit,
|
||||
cursor: cursor as i32,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
//! Independent native Wayland IME. librime and candidate rasterization never
|
||||
//! execute on Ferese's compositor thread.
|
||||
mod watchdog;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, Write};
|
||||
use std::os::fd::{AsFd, AsRawFd};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::{self, Receiver};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -13,6 +15,7 @@ use std::time::{Duration, Instant};
|
||||
use ferese_rime::{
|
||||
Engine, Snapshot,
|
||||
panel::{Colors, Panel},
|
||||
profiles::{Context, Profile, Profiles},
|
||||
};
|
||||
use wayland_client::protocol::{
|
||||
wl_buffer, wl_compositor, wl_keyboard, wl_registry, wl_seat, wl_shm, wl_shm_pool, wl_surface,
|
||||
@@ -60,6 +63,8 @@ struct State {
|
||||
engine: Engine,
|
||||
panel: Panel,
|
||||
theme: Receiver<Colors>,
|
||||
profiles: Profiles,
|
||||
control: UnixListener,
|
||||
compositor: Option<wl_compositor::WlCompositor>,
|
||||
shm: Option<wl_shm::WlShm>,
|
||||
seat: Option<wl_seat::WlSeat>,
|
||||
@@ -77,6 +82,7 @@ struct State {
|
||||
serial: u32,
|
||||
active: bool,
|
||||
pending_active: bool,
|
||||
new_context: bool,
|
||||
sensitive: bool,
|
||||
pending_sensitive: bool,
|
||||
last: Snapshot,
|
||||
@@ -123,6 +129,27 @@ impl State {
|
||||
self.routes.clear();
|
||||
return;
|
||||
}
|
||||
if self.new_context {
|
||||
self.new_context = false;
|
||||
let context = ferese_ipc::Connection::connect()
|
||||
.ok()
|
||||
.and_then(|mut c| c.call("get-input-context", serde_json::json!({})).ok())
|
||||
.and_then(|v| serde_json::from_value::<Context>(v).ok());
|
||||
let profile = self.profiles.focus(context);
|
||||
if let Err(error) = self.profiles.flush() {
|
||||
eprintln!(
|
||||
"{}",
|
||||
serde_json::json!({"event":"preferences-error","error":error})
|
||||
);
|
||||
}
|
||||
if let Err(error) = self.engine.set_profile(&profile) {
|
||||
eprintln!(
|
||||
"{}",
|
||||
serde_json::json!({"event":"profile-error","error":error})
|
||||
);
|
||||
}
|
||||
self.last = self.engine.current();
|
||||
}
|
||||
if self.grab.is_some() {
|
||||
return;
|
||||
}
|
||||
@@ -184,6 +211,21 @@ impl State {
|
||||
let keysym = xkb.key_get_one_sym(xkb::Keycode::new(key + 8)).raw();
|
||||
let repeatable = xkb.get_keymap().key_repeats(xkb::Keycode::new(key + 8));
|
||||
let mask = self.mask() | if pressed { 0 } else { 1 << 30 };
|
||||
if pressed && keysym == 0x20 && mask & (1 << 2) != 0 && mask & ((1 << 3) | (1 << 26)) == 0 {
|
||||
let profile = Profile {
|
||||
ascii: !self.last.ascii,
|
||||
schema: self.last.schema.clone(),
|
||||
};
|
||||
if let Err(error) = self.engine.set_profile(&profile) {
|
||||
self.error = Some(error);
|
||||
return;
|
||||
}
|
||||
self.routes.insert(key, false);
|
||||
self.repeat = None;
|
||||
let snapshot = self.engine.current();
|
||||
self.publish(snapshot, qh);
|
||||
return;
|
||||
}
|
||||
let (keysym, mask, paging) =
|
||||
ferese_rime::paging_key(keysym, mask, !self.last.candidates.is_empty());
|
||||
let mut snapshot = self.engine.process(keysym, mask);
|
||||
@@ -216,6 +258,17 @@ impl State {
|
||||
}
|
||||
|
||||
fn publish(&mut self, snapshot: Snapshot, qh: &QueueHandle<Self>) {
|
||||
if !snapshot.schema.is_empty()
|
||||
&& let Err(error) = self.profiles.remember(Profile {
|
||||
ascii: snapshot.ascii,
|
||||
schema: snapshot.schema.clone(),
|
||||
})
|
||||
{
|
||||
eprintln!(
|
||||
"{}",
|
||||
serde_json::json!({"event":"preferences-error","error":error})
|
||||
);
|
||||
}
|
||||
let method = self.method.as_ref().unwrap();
|
||||
if !snapshot.commit.is_empty() {
|
||||
method.commit_string(snapshot.commit.clone());
|
||||
@@ -226,7 +279,7 @@ impl State {
|
||||
eprintln!(
|
||||
"{}",
|
||||
serde_json::json!({"event":"composition", "preedit_chars":snapshot.preedit.chars().count(),
|
||||
"candidates":snapshot.candidates.len(), "committed_chars":snapshot.commit.chars().count(), "ascii":snapshot.ascii, "page":snapshot.page})
|
||||
"candidates":snapshot.candidates.len(), "committed_chars":snapshot.commit.chars().count(), "ascii":snapshot.ascii, "schema":snapshot.schema, "page":snapshot.page})
|
||||
);
|
||||
self.last = snapshot;
|
||||
if !visible {
|
||||
@@ -290,6 +343,25 @@ impl State {
|
||||
}
|
||||
|
||||
fn tick(&mut self, qh: &QueueHandle<Self>) {
|
||||
// Control is local and contains preferences only, never typed text.
|
||||
while let Ok((mut stream, _)) = self.control.accept() {
|
||||
let _ = stream.set_read_timeout(Some(Duration::from_millis(50)));
|
||||
let _ = stream.set_write_timeout(Some(Duration::from_millis(50)));
|
||||
let mut bytes = [0u8; 4096];
|
||||
let result = stream
|
||||
.read(&mut bytes)
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|n| {
|
||||
serde_json::from_slice::<serde_json::Value>(&bytes[..n])
|
||||
.map_err(|e| e.to_string())
|
||||
})
|
||||
.and_then(|request| self.control_request(request, qh));
|
||||
let response = match result {
|
||||
Ok(value) => serde_json::json!({"ok":true,"state":value}),
|
||||
Err(error) => serde_json::json!({"ok":false,"error":error}),
|
||||
};
|
||||
let _ = writeln!(stream, "{response}");
|
||||
}
|
||||
let mut changed = false;
|
||||
while let Ok(colors) = self.theme.try_recv() {
|
||||
self.panel.colors = colors;
|
||||
@@ -323,6 +395,34 @@ impl State {
|
||||
self.publish(snapshot, qh);
|
||||
}
|
||||
}
|
||||
|
||||
fn control_request(
|
||||
&mut self,
|
||||
request: serde_json::Value,
|
||||
qh: &QueueHandle<Self>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
if request["command"] == "set" {
|
||||
if !self.active || self.sensitive {
|
||||
return Err("no ordinary text input is active".into());
|
||||
}
|
||||
let mut profile = self.profiles.current.clone();
|
||||
if let Some(schema) = request["schema"].as_str() {
|
||||
profile.schema = schema.to_owned();
|
||||
}
|
||||
if let Some(ascii) = request["ascii"].as_bool() {
|
||||
profile.ascii = ascii;
|
||||
}
|
||||
self.engine.set_profile(&profile)?;
|
||||
let snapshot = self.engine.current();
|
||||
self.publish(snapshot, qh);
|
||||
} else if request["command"] != "status" {
|
||||
return Err("unknown input-method command".into());
|
||||
}
|
||||
Ok(
|
||||
serde_json::json!({"active":self.active,"sensitive":self.sensitive,
|
||||
"context":self.profiles.context,"profile":self.profiles.current,"pid":std::process::id()}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, ()> for State {
|
||||
@@ -377,6 +477,7 @@ impl Dispatch<method::ZwpInputMethodV2, ()> for State {
|
||||
method::Event::Activate => {
|
||||
state.pending_active = true;
|
||||
state.pending_sensitive = false;
|
||||
state.new_context = true;
|
||||
}
|
||||
method::Event::Deactivate => state.pending_active = false,
|
||||
method::Event::ContentType { hint, purpose } => {
|
||||
@@ -555,6 +656,13 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut shared = PathBuf::from("/usr/share/rime-data");
|
||||
let mut schema = "luna_pinyin_simp".to_owned();
|
||||
let mut self_test = false;
|
||||
let mut control = std::env::var_os("FERESE_RIME_CONTROL")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| {
|
||||
PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap_or_else(|| "/tmp".into()))
|
||||
.join("ferese/rime-control.sock")
|
||||
});
|
||||
let mut command = None;
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
@@ -562,19 +670,46 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
"--shared-data" => shared = args.next().ok_or("missing shared data path")?.into(),
|
||||
"--schema" => schema = args.next().ok_or("missing Rime schema")?,
|
||||
"--self-test" => self_test = true,
|
||||
"--control" => control = args.next().ok_or("missing control path")?.into(),
|
||||
"--status" => command = Some(serde_json::json!({"command":"status"})),
|
||||
"--mode" => {
|
||||
let mode = args.next().ok_or("missing mode")?;
|
||||
if !matches!(mode.as_str(), "ascii" | "rime") {
|
||||
return Err("mode must be ascii or rime".into());
|
||||
}
|
||||
command = Some(serde_json::json!({"command":"set","ascii":mode=="ascii"}));
|
||||
}
|
||||
"--select-schema" => {
|
||||
command = Some(
|
||||
serde_json::json!({"command":"set","schema":args.next().ok_or("missing schema")?}),
|
||||
)
|
||||
}
|
||||
"--version" => {
|
||||
println!("ferese-rime {}", env!("CARGO_PKG_VERSION"));
|
||||
return Ok(());
|
||||
}
|
||||
"--help" => {
|
||||
println!(
|
||||
"ferese-rime [--user-data PATH] [--shared-data PATH] [--schema NAME] [--self-test]"
|
||||
"ferese-rime [--user-data PATH] [--shared-data PATH] [--schema NAME] [--control PATH]\n --status | --mode ascii|rime | --select-schema NAME | --self-test"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
_ => return Err(format!("unknown option {arg}").into()),
|
||||
}
|
||||
}
|
||||
if let Some(command) = command {
|
||||
let mut stream = UnixStream::connect(&control)?;
|
||||
stream.set_read_timeout(Some(Duration::from_secs(3)))?;
|
||||
writeln!(stream, "{command}")?;
|
||||
stream.shutdown(std::net::Shutdown::Write)?;
|
||||
let mut response = String::new();
|
||||
stream.read_to_string(&mut response)?;
|
||||
println!("{}", response.trim());
|
||||
if serde_json::from_str::<serde_json::Value>(&response)?["ok"] != true {
|
||||
return Err("input-method command failed".into());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
std::fs::create_dir_all(&user)?;
|
||||
std::fs::set_permissions(&user, std::fs::Permissions::from_mode(0o700))?;
|
||||
let mut engine = Engine::open(&user, &shared, &schema)?;
|
||||
@@ -598,6 +733,21 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let profiles = Profiles::open(&user.join("input-profiles.json"), schema)?;
|
||||
let parent = control.parent().ok_or("control path has no parent")?;
|
||||
std::fs::create_dir_all(parent)?;
|
||||
if control.exists() {
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
if !std::fs::symlink_metadata(&control)?.file_type().is_socket()
|
||||
|| UnixStream::connect(&control).is_ok()
|
||||
{
|
||||
return Err("input-method control path is already in use".into());
|
||||
}
|
||||
std::fs::remove_file(&control)?;
|
||||
}
|
||||
let listener = UnixListener::bind(&control)?;
|
||||
std::fs::set_permissions(&control, std::fs::Permissions::from_mode(0o600))?;
|
||||
listener.set_nonblocking(true)?;
|
||||
let panel = Panel::new()?;
|
||||
let (send, theme) = mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
@@ -625,6 +775,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
engine,
|
||||
panel,
|
||||
theme,
|
||||
profiles,
|
||||
control: listener,
|
||||
compositor: None,
|
||||
shm: None,
|
||||
seat: None,
|
||||
@@ -642,6 +794,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
serial: 0,
|
||||
active: false,
|
||||
pending_active: false,
|
||||
new_context: false,
|
||||
sensitive: false,
|
||||
pending_sensitive: false,
|
||||
last: Snapshot::default(),
|
||||
@@ -652,9 +805,13 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
};
|
||||
queue.roundtrip(&mut state)?;
|
||||
state.initialize(&qh)?;
|
||||
let mut watchdog = watchdog::Watchdog::from_environment();
|
||||
loop {
|
||||
queue.dispatch_pending(&mut state)?;
|
||||
state.tick(&qh);
|
||||
if let Some(watchdog) = &mut watchdog {
|
||||
watchdog.tick();
|
||||
}
|
||||
if let Some(error) = state.error.take() {
|
||||
return Err(error.into());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
//! Window-local mode/schema preferences, with application defaults for reopening.
|
||||
//! Persist preferences only: never composition, surrounding text or window titles.
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct Profile {
|
||||
pub schema: String,
|
||||
pub ascii: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Context {
|
||||
pub session: String,
|
||||
pub window: u64,
|
||||
pub app_id: String,
|
||||
}
|
||||
impl Context {
|
||||
fn key(&self) -> String {
|
||||
format!("{}:{}", self.session, self.window)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
struct Stored {
|
||||
#[serde(default)]
|
||||
applications: BTreeMap<String, Profile>,
|
||||
#[serde(default)]
|
||||
windows: BTreeMap<String, Profile>,
|
||||
}
|
||||
|
||||
pub struct Profiles {
|
||||
path: PathBuf,
|
||||
default: Profile,
|
||||
stored: Stored,
|
||||
dirty: bool,
|
||||
pub context: Option<Context>,
|
||||
pub current: Profile,
|
||||
}
|
||||
impl Profiles {
|
||||
pub fn open(path: &Path, schema: String) -> Result<Self, String> {
|
||||
let stored = match std::fs::read(path) {
|
||||
Ok(bytes) => serde_json::from_slice(&bytes)
|
||||
.map_err(|e| format!("invalid IME preferences: {e}"))?,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Stored::default(),
|
||||
Err(e) => return Err(e.to_string()),
|
||||
};
|
||||
let default = Profile {
|
||||
schema,
|
||||
ascii: false,
|
||||
};
|
||||
Ok(Self {
|
||||
path: path.to_owned(),
|
||||
stored,
|
||||
dirty: false,
|
||||
context: None,
|
||||
current: default.clone(),
|
||||
default,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn focus(&mut self, context: Option<Context>) -> Profile {
|
||||
if let Some(context) = &context {
|
||||
let prefix = format!("{}:", context.session);
|
||||
self.stored
|
||||
.windows
|
||||
.retain(|key, _| key.starts_with(&prefix));
|
||||
}
|
||||
let profile = context
|
||||
.as_ref()
|
||||
.and_then(|c| {
|
||||
self.stored
|
||||
.windows
|
||||
.get(&c.key())
|
||||
.or_else(|| self.stored.applications.get(&c.app_id))
|
||||
})
|
||||
.unwrap_or(&self.default)
|
||||
.clone();
|
||||
self.context = context;
|
||||
self.current = profile.clone();
|
||||
// Pin even the initial default. A second window of the same application
|
||||
// can change its mode without changing this already-open window.
|
||||
if let Some(context) = &self.context {
|
||||
self.dirty |= !self.stored.windows.contains_key(&context.key());
|
||||
self.stored
|
||||
.windows
|
||||
.entry(context.key())
|
||||
.or_insert_with(|| profile.clone());
|
||||
}
|
||||
profile
|
||||
}
|
||||
|
||||
pub fn remember(&mut self, profile: Profile) -> Result<(), String> {
|
||||
if self.current == profile {
|
||||
return self.flush();
|
||||
}
|
||||
self.current = profile.clone();
|
||||
let Some(context) = &self.context else {
|
||||
return Ok(());
|
||||
};
|
||||
self.stored.windows.insert(context.key(), profile.clone());
|
||||
if !context.app_id.is_empty() {
|
||||
self.stored
|
||||
.applications
|
||||
.insert(context.app_id.clone(), profile);
|
||||
}
|
||||
self.dirty = true;
|
||||
self.flush()
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) -> Result<(), String> {
|
||||
if self.dirty {
|
||||
self.save()?;
|
||||
self.dirty = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save(&self) -> Result<(), String> {
|
||||
let parent = self.path.parent().ok_or("preferences path has no parent")?;
|
||||
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||
let mut temp = tempfile::NamedTempFile::new_in(parent).map_err(|e| e.to_string())?;
|
||||
temp.as_file()
|
||||
.set_permissions(std::fs::Permissions::from_mode(0o600))
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_writer_pretty(&mut temp, &self.stored).map_err(|e| e.to_string())?;
|
||||
temp.write_all(b"\n")
|
||||
.and_then(|()| temp.as_file().sync_all())
|
||||
.map_err(|e| e.to_string())?;
|
||||
temp.persist(&self.path).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn context(window: u64, app: &str) -> Option<Context> {
|
||||
Some(Context {
|
||||
session: "one".into(),
|
||||
window,
|
||||
app_id: app.into(),
|
||||
})
|
||||
}
|
||||
#[test]
|
||||
fn isolates_windows_restores_closed_apps_and_survives_service_restart() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("preferences.json");
|
||||
let rime = Profile {
|
||||
schema: "luna_pinyin_simp".into(),
|
||||
ascii: false,
|
||||
};
|
||||
let english = Profile {
|
||||
ascii: true,
|
||||
..rime.clone()
|
||||
};
|
||||
let other = Profile {
|
||||
schema: "luna_pinyin".into(),
|
||||
ascii: false,
|
||||
};
|
||||
let mut p = Profiles::open(&path, rime.schema.clone()).unwrap();
|
||||
assert_eq!(p.focus(context(1, "QQ")), rime);
|
||||
p.remember(english.clone()).unwrap();
|
||||
assert_eq!(p.focus(context(2, "QQ")), english);
|
||||
p.remember(other.clone()).unwrap();
|
||||
assert_eq!(p.focus(context(1, "QQ")), english);
|
||||
assert_eq!(p.focus(context(3, "teams")), rime);
|
||||
drop(p);
|
||||
let mut p = Profiles::open(&path, rime.schema.clone()).unwrap();
|
||||
assert_eq!(p.focus(context(1, "QQ")), english);
|
||||
assert_eq!(p.focus(context(2, "QQ")), other);
|
||||
assert_eq!(p.focus(context(4, "QQ")), other);
|
||||
let mut fresh = context(1, "QQ").unwrap();
|
||||
fresh.session = "two".into();
|
||||
assert_eq!(p.focus(Some(fresh)), other);
|
||||
assert_eq!(p.focus(None), rime);
|
||||
assert_eq!(
|
||||
std::fs::metadata(path).unwrap().permissions().mode() & 0o777,
|
||||
0o600
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
/* Keep the versioned librime ABI in this small, header-checked boundary. */
|
||||
typedef struct {
|
||||
int accepted, ascii_mode, cursor, highlighted, page_no, last_page, count;
|
||||
char *commit, *preedit;
|
||||
char *commit, *preedit, *schema;
|
||||
char *texts[9], *comments[9];
|
||||
} FereseRimeResult;
|
||||
|
||||
@@ -57,15 +57,22 @@ void ferese_rime_snapshot(int key, int mask, FereseRimeResult *out) {
|
||||
RIME_STRUCT(RimeStatus, status);
|
||||
if (api->get_status(session, &status)) {
|
||||
out->ascii_mode = status.is_ascii_mode;
|
||||
out->schema = strdup(status.schema_id ? status.schema_id : "");
|
||||
api->free_status(&status);
|
||||
}
|
||||
}
|
||||
|
||||
void ferese_rime_result_free(FereseRimeResult *out) {
|
||||
free(out->commit); free(out->preedit);
|
||||
free(out->commit); free(out->preedit); free(out->schema);
|
||||
for (int n = 0; n < out->count; n++) { free(out->texts[n]); free(out->comments[n]); }
|
||||
}
|
||||
void ferese_rime_clear(void) { if (session) api->clear_composition(session); }
|
||||
int ferese_rime_profile(const char *schema, int ascii_mode) {
|
||||
if (!api->select_schema(session, schema)) return 0;
|
||||
api->clear_composition(session);
|
||||
api->set_option(session, "ascii_mode", ascii_mode);
|
||||
return 1;
|
||||
}
|
||||
void ferese_rime_close(void) {
|
||||
if (session) api->destroy_session(session);
|
||||
if (api) api->finalize();
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
//! Heartbeat from the input-processing loop: a blocked engine cannot fake health.
|
||||
use std::os::linux::net::SocketAddrExt;
|
||||
use std::os::unix::net::{SocketAddr, UnixDatagram};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub struct Watchdog {
|
||||
socket: UnixDatagram,
|
||||
interval: Duration,
|
||||
next: Instant,
|
||||
}
|
||||
impl Watchdog {
|
||||
pub fn from_environment() -> Option<Self> {
|
||||
let path = std::env::var("NOTIFY_SOCKET").ok()?;
|
||||
let micros = std::env::var("WATCHDOG_USEC").ok()?.parse::<u64>().ok()?;
|
||||
if micros == 0 {
|
||||
return None;
|
||||
}
|
||||
let address = if let Some(name) = path.strip_prefix('@') {
|
||||
SocketAddr::from_abstract_name(name.as_bytes()).ok()?
|
||||
} else {
|
||||
SocketAddr::from_pathname(path).ok()?
|
||||
};
|
||||
let socket = UnixDatagram::unbound().ok()?;
|
||||
socket.connect_addr(&address).ok()?;
|
||||
socket.set_nonblocking(true).ok()?;
|
||||
socket.send(b"READY=1\nWATCHDOG=1").ok()?;
|
||||
Some(Self {
|
||||
socket,
|
||||
interval: Duration::from_micros((micros / 3).max(1)),
|
||||
next: Instant::now(),
|
||||
})
|
||||
}
|
||||
pub fn tick(&mut self) {
|
||||
if Instant::now() >= self.next {
|
||||
let _ = self.socket.send(b"WATCHDOG=1");
|
||||
self.next = Instant::now() + self.interval;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user