use core::ffi::c_char;
use crate::MimeType;
use super::ffi::{self, Connection, Lib};
macro_rules! atom_table {
($($field:ident => $name:literal),+ $(,)?) => {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[allow(
missing_docs,
reason = "each field is named by the wire string it holds, which is \
in the table below and in the module docs; a doc comment \
per field would restate the literal"
)]
pub struct Atoms {
$(pub $field: u32,)+
}
const NAMES: &[&str] = &[$($name,)+];
impl Atoms {
fn from_values(values: &[u32]) -> Self {
let mut index = 0;
#[allow(unused_assignments, reason = "the last increment is dead by construction")]
Self {
$($field: { let value = values.get(index).copied().unwrap_or(0); index += 1; value },)+
}
}
}
};
}
atom_table! {
wm_protocols => "WM_PROTOCOLS",
wm_delete_window => "WM_DELETE_WINDOW",
net_wm_name => "_NET_WM_NAME",
net_wm_state => "_NET_WM_STATE",
net_wm_state_fullscreen => "_NET_WM_STATE_FULLSCREEN",
net_supporting_wm_check => "_NET_SUPPORTING_WM_CHECK",
net_workarea => "_NET_WORKAREA",
net_wm_pid => "_NET_WM_PID",
utf8_string => "UTF8_STRING",
clipboard => "CLIPBOARD",
targets => "TARGETS",
timestamp => "TIMESTAMP",
multiple => "MULTIPLE",
atom_pair => "ATOM_PAIR",
incr => "INCR",
crcbl_selection => "CRCBL_SELECTION",
crcbl_timestamp => "CRCBL_TIMESTAMP",
xdnd_aware => "XdndAware",
xdnd_enter => "XdndEnter",
xdnd_position => "XdndPosition",
xdnd_status => "XdndStatus",
xdnd_leave => "XdndLeave",
xdnd_drop => "XdndDrop",
xdnd_finished => "XdndFinished",
xdnd_selection => "XdndSelection",
xdnd_type_list => "XdndTypeList",
xdnd_action_copy => "XdndActionCopy",
crcbl_xdnd => "CRCBL_XDND",
mime_text_utf8 => "text/plain;charset=utf-8",
mime_text_plain => "text/plain",
mime_crcbl_ron => "application/x-crcbl+ron",
mime_uri_list => "text/uri-list",
}
impl Atoms {
#[must_use]
pub unsafe fn intern(lib: &'static Lib, connection: *mut Connection) -> Self {
let cookies: Vec<ffi::Cookie> = NAMES
.iter()
.map(|name| {
let len = u16::try_from(name.len()).unwrap_or(u16::MAX);
unsafe { (lib.intern_atom)(connection, 0, len, name.as_ptr().cast::<c_char>()) }
})
.collect();
let values: Vec<u32> = cookies
.into_iter()
.map(|cookie| {
let reply =
unsafe { (lib.intern_atom_reply)(connection, cookie, core::ptr::null_mut()) };
if reply.is_null() {
return 0;
}
let atom = unsafe { (*reply).atom };
unsafe { ffi::free_reply(reply) };
atom
})
.collect();
Self::from_values(&values)
}
#[must_use]
pub fn for_mime(&self, mime: MimeType) -> u32 {
match mime {
MimeType::TextUtf8 => self.mime_text_utf8,
MimeType::CrcblRon => self.mime_crcbl_ron,
MimeType::UriList => self.mime_uri_list,
MimeType::Other(_) => 0,
}
}
#[must_use]
pub fn text_targets(&self) -> [u32; 4] {
[
self.mime_text_utf8,
self.utf8_string,
self.mime_text_plain,
ffi::value::ATOM_STRING,
]
}
#[must_use]
pub fn candidates_for(&self, mime: MimeType) -> Vec<u32> {
let mut candidates = match mime {
MimeType::TextUtf8 => self.text_targets().to_vec(),
other => vec![self.for_mime(other)],
};
candidates.retain(|atom| *atom != 0);
candidates
}
#[must_use]
pub fn mime_for_target(&self, target: u32) -> Option<MimeType> {
if target == 0 {
return None;
}
if self.text_targets().contains(&target) {
return Some(MimeType::TextUtf8);
}
if target == self.mime_crcbl_ron {
return Some(MimeType::CrcblRon);
}
if target == self.mime_uri_list {
return Some(MimeType::UriList);
}
None
}
#[must_use]
pub fn spelling_of(&self, target: u32) -> Option<&'static str> {
let known = [
(self.mime_text_utf8, MimeType::TextUtf8.as_str()),
(self.utf8_string, "UTF8_STRING"),
(self.mime_text_plain, "text/plain"),
(ffi::value::ATOM_STRING, "STRING"),
(self.mime_crcbl_ron, MimeType::CrcblRon.as_str()),
(self.mime_uri_list, MimeType::UriList.as_str()),
];
known
.into_iter()
.find(|(atom, _)| *atom != 0 && *atom == target)
.map(|(_, name)| name)
}
}
#[cfg(test)]
mod tests {
use super::*;
const LAST_PREDEFINED: u32 = 68;
fn table() -> Atoms {
let values: Vec<u32> = (1..=u32::try_from(NAMES.len()).expect("small"))
.map(|index| LAST_PREDEFINED + index)
.collect();
Atoms::from_values(&values)
}
#[test]
fn the_table_is_filled_in_name_order() {
let atoms = table();
assert_eq!(atoms.wm_protocols, LAST_PREDEFINED + 1, "the first name");
assert_eq!(atoms.wm_delete_window, LAST_PREDEFINED + 2);
assert_eq!(
atoms.mime_uri_list,
LAST_PREDEFINED + u32::try_from(NAMES.len()).expect("small"),
"the last name"
);
let mut seen: Vec<u32> = vec![
atoms.wm_protocols,
atoms.wm_delete_window,
atoms.net_wm_name,
atoms.net_wm_state,
atoms.net_wm_state_fullscreen,
atoms.net_supporting_wm_check,
atoms.net_workarea,
atoms.net_wm_pid,
atoms.utf8_string,
atoms.clipboard,
atoms.targets,
atoms.timestamp,
atoms.multiple,
atoms.atom_pair,
atoms.incr,
atoms.crcbl_selection,
atoms.crcbl_timestamp,
atoms.xdnd_aware,
atoms.xdnd_enter,
atoms.xdnd_position,
atoms.xdnd_status,
atoms.xdnd_leave,
atoms.xdnd_drop,
atoms.xdnd_finished,
atoms.xdnd_selection,
atoms.xdnd_type_list,
atoms.xdnd_action_copy,
atoms.crcbl_xdnd,
atoms.mime_text_utf8,
atoms.mime_text_plain,
atoms.mime_crcbl_ron,
atoms.mime_uri_list,
];
assert_eq!(seen.len(), NAMES.len(), "every name has a field");
seen.sort_unstable();
seen.dedup();
assert_eq!(seen.len(), NAMES.len(), "no field is filled twice");
}
#[test]
fn a_server_that_refuses_an_intern_leaves_a_zero_rather_than_shifting() {
let atoms = Atoms::from_values(&[7, 9]);
assert_eq!(atoms.wm_protocols, 7);
assert_eq!(atoms.wm_delete_window, 9);
assert_eq!(atoms.net_wm_name, 0);
assert_eq!(atoms.mime_uri_list, 0);
assert_eq!(Atoms::default(), Atoms::from_values(&[]));
}
#[test]
fn the_engine_mime_types_map_to_atoms_and_back() {
let atoms = table();
for mime in [MimeType::TextUtf8, MimeType::CrcblRon, MimeType::UriList] {
let target = atoms.for_mime(mime);
assert_ne!(target, 0, "{mime}");
assert_eq!(atoms.mime_for_target(target), Some(mime), "{mime}");
assert_eq!(atoms.spelling_of(target), Some(mime.as_str()), "{mime}");
}
assert_eq!(atoms.for_mime(MimeType::Other("image/png")), 0);
assert_eq!(atoms.mime_for_target(0), None, "ATOM_NONE is not a format");
}
#[test]
fn a_read_asks_for_every_spelling_of_text_and_one_of_everything_else() {
let atoms = table();
assert_eq!(
atoms.candidates_for(MimeType::TextUtf8),
atoms.text_targets().to_vec(),
"four spellings, in preference order"
);
assert_eq!(
atoms.candidates_for(MimeType::CrcblRon),
vec![atoms.mime_crcbl_ron],
"the engine's own format has exactly one spelling"
);
assert!(
atoms
.candidates_for(MimeType::Other("image/png"))
.is_empty(),
"a format with no atom cannot be asked for"
);
}
#[test]
fn every_text_spelling_an_x11_peer_uses_is_accepted() {
let atoms = table();
for target in atoms.text_targets() {
assert_eq!(
atoms.mime_for_target(target),
Some(MimeType::TextUtf8),
"atom {target}"
);
}
assert_eq!(
atoms.text_targets()[0],
atoms.mime_text_utf8,
"the most specific spelling is advertised first"
);
assert_eq!(
atoms.text_targets()[3],
ffi::value::ATOM_STRING,
"Latin-1 STRING is the last resort, and is a predefined atom"
);
assert_eq!(atoms.spelling_of(atoms.utf8_string), Some("UTF8_STRING"));
assert_eq!(
atoms.spelling_of(ffi::value::ATOM_STRING),
Some("STRING"),
"a predefined atom still has a spelling"
);
assert_eq!(atoms.spelling_of(999_999), None, "an atom we never offered");
}
}