use std::ops::Range;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
use teksilo_core::raw_handle::ParentHandle;
use teksilo_core::text_surface::TextSurfaces;
use teksilo_core::window::TeksiloWindowId;
use teksilo_core::{AppEventPoster, ImeOrigin};
mod answers;
mod composition;
mod dictation;
mod imm_completion;
mod imm_requests;
mod key_press;
mod key_trace;
mod left_behind;
#[cfg(target_os = "macos")]
mod macos;
mod tsf_lock;
mod tsf_mirror;
#[cfg(target_os = "windows")]
mod windows;
mod winit_ime;
mod write_half;
pub fn trace_enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| {
std::env::var_os("TEKSILO_TEXT_INPUT_TRACE").is_some_and(|value| value != "0")
})
}
#[allow(dead_code)] pub(crate) fn trace_stacks_enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| {
std::env::var_os("TEKSILO_TEXT_INPUT_TRACE").is_some_and(|value| value == "stacks")
})
}
#[allow(dead_code)] pub(crate) fn trace(args: std::fmt::Arguments<'_>) {
if trace_enabled() {
eprintln!("[text-input] {args}");
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImeCommitPayload {
pub window_id_owner: TeksiloWindowId,
pub text: String,
pub origin: ImeOrigin,
pub replacement: Option<Range<usize>>,
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ImeWrittenPayload {
pub window_id_owner: TeksiloWindowId,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[cfg_attr(
not(target_os = "macos"),
allow(dead_code, reason = "only the macOS hook classifies, for now")
)]
pub(crate) struct CommitSignals {
pub(crate) had_marked_text: bool,
pub(crate) carries_text_alternatives: bool,
pub(crate) dictation_session: bool,
pub(crate) writing_tools_session: bool,
}
#[cfg_attr(
not(target_os = "macos"),
allow(dead_code, reason = "only the macOS hook classifies, for now")
)]
pub(crate) fn classify(signals: CommitSignals) -> ImeOrigin {
if signals.carries_text_alternatives || signals.dictation_session {
ImeOrigin::Dictation
} else if signals.writing_tools_session {
ImeOrigin::WritingTools
} else if signals.had_marked_text {
ImeOrigin::Composition
} else {
ImeOrigin::Insertion
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TakenKey {
Swallowed,
Committed,
Substituted(String),
}
pub fn key_taken_by_input_method(window_id: TeksiloWindowId) -> Option<TakenKey> {
#[cfg(target_os = "macos")]
{
macos::key_taken_by_input_method(window_id)
}
#[cfg(not(target_os = "macos"))]
{
let _ = window_id;
None
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FinishedComposition {
pub committed: Option<String>,
pub late_commits: Vec<String>,
}
pub fn finish_composition(window_id: TeksiloWindowId) -> Option<FinishedComposition> {
#[cfg(target_os = "macos")]
{
macos::finish_composition(window_id).then(FinishedComposition::default)
}
#[cfg(target_os = "windows")]
{
windows::finish_composition(window_id)
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
let _ = window_id;
None
}
}
pub fn take_marked_replacement(window_id: TeksiloWindowId) -> Option<Range<usize>> {
#[cfg(target_os = "macos")]
{
macos::take_marked_replacement(window_id)
}
#[cfg(not(target_os = "macos"))]
{
let _ = window_id;
None
}
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextStoreMode {
Off,
Store,
StoreKeepingImm,
}
static STORE_PREFERRED: AtomicBool = AtomicBool::new(true);
#[doc(hidden)]
pub fn set_text_store_preferred(preferred: bool) {
STORE_PREFERRED.store(preferred, Ordering::Relaxed);
}
#[doc(hidden)]
pub fn text_store_mode() -> TextStoreMode {
static FROM_ENVIRONMENT: OnceLock<Option<TextStoreMode>> = OnceLock::new();
if !cfg!(target_os = "windows") {
return TextStoreMode::Off;
}
let from_environment = *FROM_ENVIRONMENT
.get_or_init(|| parse_text_store_mode(std::env::var("TEKSILO_TSF").ok().as_deref()));
from_environment.unwrap_or(if STORE_PREFERRED.load(Ordering::Relaxed) {
TextStoreMode::Store
} else {
TextStoreMode::Off
})
}
#[doc(hidden)]
pub fn text_store_mode_for(window: &ParentHandle) -> TextStoreMode {
#[cfg(target_os = "windows")]
{
windows::text_store_mode_for(window)
}
#[cfg(not(target_os = "windows"))]
{
let _ = window;
TextStoreMode::Off
}
}
#[doc(hidden)]
pub fn legacy_input_method_active() -> bool {
#[cfg(target_os = "windows")]
{
windows::legacy_input_method_active()
}
#[cfg(not(target_os = "windows"))]
{
false
}
}
fn parse_text_store_mode(value: Option<&str>) -> Option<TextStoreMode> {
match value.map(str::trim) {
Some("0" | "off") => Some(TextStoreMode::Off),
Some("1" | "store") => Some(TextStoreMode::Store),
Some("associated") => Some(TextStoreMode::StoreKeepingImm),
_ => None,
}
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StoreFocus {
None,
Secure {
widget: teksilo_core::WidgetId,
},
Imm,
Document {
widget: teksilo_core::WidgetId,
},
}
#[doc(hidden)]
pub fn terminate_text_store_composition(window: &ParentHandle) -> bool {
#[cfg(target_os = "windows")]
{
windows::terminate_text_store_composition(window)
}
#[cfg(not(target_os = "windows"))]
{
let _ = window;
false
}
}
#[doc(hidden)]
pub fn take_key_taken_by_text_store() -> bool {
#[cfg(target_os = "windows")]
{
windows::take_key_taken_by_text_store()
}
#[cfg(not(target_os = "windows"))]
{
false
}
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TextStorePayload {
pub window_id_owner: TeksiloWindowId,
}
#[doc(hidden)]
pub fn set_text_store_focus(window: &ParentHandle, focus: StoreFocus) {
#[cfg(target_os = "windows")]
windows::set_text_store_focus(window, focus);
#[cfg(not(target_os = "windows"))]
let _ = (window, focus);
}
pub fn attach(
window_id: TeksiloWindowId,
parent: &ParentHandle,
poster: Arc<dyn AppEventPoster>,
surfaces: TextSurfaces,
) {
#[cfg(target_os = "macos")]
macos::attach(window_id, parent, poster, surfaces);
#[cfg(target_os = "windows")]
windows::attach(window_id, parent, poster, surfaces);
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
let _ = (window_id, parent, poster, surfaces);
}
pub fn detach(window_id: TeksiloWindowId) {
#[cfg(target_os = "macos")]
macos::detach(window_id);
#[cfg(target_os = "windows")]
windows::detach(window_id);
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
let _ = window_id;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_environment_decides_the_text_store_only_when_it_names_a_mode() {
assert_eq!(parse_text_store_mode(None), None);
assert_eq!(parse_text_store_mode(Some("")), None);
assert_eq!(parse_text_store_mode(Some("0")), Some(TextStoreMode::Off));
assert_eq!(parse_text_store_mode(Some("off")), Some(TextStoreMode::Off));
assert_eq!(parse_text_store_mode(Some("1")), Some(TextStoreMode::Store));
assert_eq!(
parse_text_store_mode(Some(" store ")),
Some(TextStoreMode::Store)
);
assert_eq!(
parse_text_store_mode(Some("associated")),
Some(TextStoreMode::StoreKeepingImm)
);
}
#[test]
fn classify_follows_the_precedence_dictation_writing_tools_marked_text() {
use ImeOrigin::{Composition, Dictation, Insertion, WritingTools};
for bits in 0..16u8 {
let signals = CommitSignals {
had_marked_text: bits & 1 != 0,
carries_text_alternatives: bits & 2 != 0,
dictation_session: bits & 4 != 0,
writing_tools_session: bits & 8 != 0,
};
let expected = if signals.carries_text_alternatives || signals.dictation_session {
Dictation
} else if signals.writing_tools_session {
WritingTools
} else if signals.had_marked_text {
Composition
} else {
Insertion
};
assert_eq!(classify(signals), expected, "{signals:?}");
}
let measured = [
(CommitSignals::default(), Insertion),
(
CommitSignals {
had_marked_text: true,
..CommitSignals::default()
},
Composition,
),
(
CommitSignals {
had_marked_text: true,
dictation_session: true,
..CommitSignals::default()
},
Dictation,
),
(
CommitSignals {
writing_tools_session: true,
..CommitSignals::default()
},
WritingTools,
),
(
CommitSignals {
had_marked_text: true,
dictation_session: true,
writing_tools_session: true,
..CommitSignals::default()
},
Dictation,
),
];
for (signals, expected) in measured {
assert_eq!(classify(signals), expected, "{signals:?}");
}
}
}