From 90c32b068edf45e7c2d211174f0a158e2d6f33a4 Mon Sep 17 00:00:00 2001 From: Sawyer McLane Date: Mon, 30 Sep 2024 09:53:07 -0600 Subject: [PATCH] Added key combos to listener --- src/listener.rs | 320 +++++++++++++++++++++++++++++++++--------------- src/shortcut.rs | 48 ++++---- 2 files changed, 244 insertions(+), 124 deletions(-) diff --git a/src/listener.rs b/src/listener.rs index 37a254a..6bc9234 100644 --- a/src/listener.rs +++ b/src/listener.rs @@ -1,3 +1,4 @@ +use eframe::egui; use log::error; use rdev::{listen, Button, Event, EventType, Key}; use std::collections::BTreeSet; @@ -10,106 +11,212 @@ use std::time::Instant; pub type BackgroundCallback = Box; -#[derive(Clone, Copy, Debug)] -pub enum InputAction { +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum InputItem { Key(Key), Button(Button), - Unknown, } -impl FromStr for InputAction { - fn from_str(s: &str) -> Result { - Ok(match s.to_ascii_lowercase().as_str() { - "a" => InputAction::Key(Key::KeyA), - "b" => InputAction::Key(Key::KeyB), - "c" => InputAction::Key(Key::KeyC), - "d" => InputAction::Key(Key::KeyD), - "e" => InputAction::Key(Key::KeyE), - "f" => InputAction::Key(Key::KeyF), - "g" => InputAction::Key(Key::KeyG), - "h" => InputAction::Key(Key::KeyH), - "i" => InputAction::Key(Key::KeyI), - "j" => InputAction::Key(Key::KeyJ), - "k" => InputAction::Key(Key::KeyK), - "l" => InputAction::Key(Key::KeyL), - "m" => InputAction::Key(Key::KeyM), - "n" => InputAction::Key(Key::KeyN), - "o" => InputAction::Key(Key::KeyO), - "p" => InputAction::Key(Key::KeyP), - "q" => InputAction::Key(Key::KeyQ), - "r" => InputAction::Key(Key::KeyR), - "s" => InputAction::Key(Key::KeyS), - "t" => InputAction::Key(Key::KeyT), - "u" => InputAction::Key(Key::KeyU), - "v" => InputAction::Key(Key::KeyV), - "w" => InputAction::Key(Key::KeyW), - "x" => InputAction::Key(Key::KeyX), - "y" => InputAction::Key(Key::KeyY), - "z" => InputAction::Key(Key::KeyZ), - _ => return Err(error!("Failed to parse InputAction from string: {}", s)), - }) +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct InputAction(pub BTreeSet); + +impl FromStr for InputItem { + fn from_str(s: &str) -> Result { + match s.to_ascii_lowercase().as_str() { + "ctrl" => Ok(InputItem::Key(Key::ControlLeft)), + "alt" => Ok(InputItem::Key(Key::Alt)), + "shift" => Ok(InputItem::Key(Key::ShiftLeft)), + "cmd" | "meta" => Ok(InputItem::Key(Key::MetaLeft)), + "left" => Ok(InputItem::Button(Button::Left)), + "right" => Ok(InputItem::Button(Button::Right)), + "middle" => Ok(InputItem::Button(Button::Middle)), + "space" => Ok(InputItem::Key(Key::Space)), + "enter" | "return" => Ok(InputItem::Key(Key::Return)), + "escape" => Ok(InputItem::Key(Key::Escape)), + "tab" => Ok(InputItem::Key(Key::Tab)), + "a" => Ok(InputItem::Key(Key::KeyA)), + "b" => Ok(InputItem::Key(Key::KeyB)), + "c" => Ok(InputItem::Key(Key::KeyC)), + "d" => Ok(InputItem::Key(Key::KeyD)), + "e" => Ok(InputItem::Key(Key::KeyE)), + "f" => Ok(InputItem::Key(Key::KeyF)), + "g" => Ok(InputItem::Key(Key::KeyG)), + "h" => Ok(InputItem::Key(Key::KeyH)), + "i" => Ok(InputItem::Key(Key::KeyI)), + "j" => Ok(InputItem::Key(Key::KeyJ)), + "k" => Ok(InputItem::Key(Key::KeyK)), + "l" => Ok(InputItem::Key(Key::KeyL)), + "m" => Ok(InputItem::Key(Key::KeyM)), + "n" => Ok(InputItem::Key(Key::KeyN)), + "o" => Ok(InputItem::Key(Key::KeyO)), + "p" => Ok(InputItem::Key(Key::KeyP)), + "q" => Ok(InputItem::Key(Key::KeyQ)), + "r" => Ok(InputItem::Key(Key::KeyR)), + "s" => Ok(InputItem::Key(Key::KeyS)), + "t" => Ok(InputItem::Key(Key::KeyT)), + "u" => Ok(InputItem::Key(Key::KeyU)), + "v" => Ok(InputItem::Key(Key::KeyV)), + "w" => Ok(InputItem::Key(Key::KeyW)), + "x" => Ok(InputItem::Key(Key::KeyX)), + "y" => Ok(InputItem::Key(Key::KeyY)), + "z" => Ok(InputItem::Key(Key::KeyZ)), + _ => Err(error!("Failed to parse InputAction from string: {}", s)), + } } - type Err = (); } -impl PartialEq for InputAction { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (InputAction::Key(k1), InputAction::Key(k2)) => k1 == k2, - (InputAction::Button(b1), InputAction::Button(b2)) => b1 == b2, - _ => false, - } +#[derive(Debug)] +pub struct KeyMappingError { + key: egui::Key, +} + +impl Display for KeyMappingError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "Failed to map egui::Key to rdev::Key: {:?}", self.key) } } -impl Eq for InputAction {} +pub fn map_egui_key_to_rdev_key(key: egui::Key) -> Result { + match key { + egui::Key::ArrowDown => Ok(Key::DownArrow), + egui::Key::ArrowLeft => Ok(Key::LeftArrow), + egui::Key::ArrowRight => Ok(Key::RightArrow), + egui::Key::ArrowUp => Ok(Key::UpArrow), + egui::Key::Backspace => Ok(Key::Backspace), + egui::Key::Delete => Ok(Key::Delete), + egui::Key::End => Ok(Key::End), + egui::Key::Enter => Ok(Key::Return), + egui::Key::Escape => Ok(Key::Escape), + egui::Key::Home => Ok(Key::Home), + egui::Key::Insert => Ok(Key::Insert), + egui::Key::PageDown => Ok(Key::PageDown), + egui::Key::PageUp => Ok(Key::PageUp), + egui::Key::Space => Ok(Key::Space), + egui::Key::Tab => Ok(Key::Tab), + egui::Key::A => Ok(Key::KeyA), + egui::Key::B => Ok(Key::KeyB), + egui::Key::C => Ok(Key::KeyC), + egui::Key::D => Ok(Key::KeyD), + egui::Key::E => Ok(Key::KeyE), + egui::Key::F => Ok(Key::KeyF), + egui::Key::G => Ok(Key::KeyG), + egui::Key::H => Ok(Key::KeyH), + egui::Key::I => Ok(Key::KeyI), + egui::Key::J => Ok(Key::KeyJ), + egui::Key::K => Ok(Key::KeyK), + egui::Key::L => Ok(Key::KeyL), + egui::Key::M => Ok(Key::KeyM), + egui::Key::N => Ok(Key::KeyN), + egui::Key::O => Ok(Key::KeyO), + egui::Key::P => Ok(Key::KeyP), + egui::Key::Q => Ok(Key::KeyQ), + egui::Key::R => Ok(Key::KeyR), + egui::Key::S => Ok(Key::KeyS), + egui::Key::T => Ok(Key::KeyT), + egui::Key::U => Ok(Key::KeyU), + egui::Key::V => Ok(Key::KeyV), + egui::Key::W => Ok(Key::KeyW), + egui::Key::X => Ok(Key::KeyX), + egui::Key::Y => Ok(Key::KeyY), + egui::Key::Z => Ok(Key::KeyZ), + _ => Err(KeyMappingError { key }), + } +} -impl Hash for InputAction { +impl Hash for InputItem { fn hash(&self, state: &mut H) { std::mem::discriminant(self).hash(state); match self { - InputAction::Key(k) => k.hash(state), - InputAction::Button(b) => b.hash(state), - InputAction::Unknown => "Unknown".hash(state), + InputItem::Key(k) => k.hash(state), + InputItem::Button(b) => b.hash(state), } } } -impl Ord for InputAction { +impl Ord for InputItem { fn cmp(&self, other: &Self) -> std::cmp::Ordering { match (self, other) { - (InputAction::Key(k1), InputAction::Key(k2)) => { + (InputItem::Key(k1), InputItem::Key(k2)) => { format!("{:?}", k1).cmp(&format!("{:?}", k2)) } - (InputAction::Button(b1), InputAction::Button(b2)) => { + (InputItem::Button(b1), InputItem::Button(b2)) => { format!("{:?}", b1).cmp(&format!("{:?}", b2)) } - (InputAction::Key(_), InputAction::Button(_)) => std::cmp::Ordering::Less, - (InputAction::Button(_), InputAction::Key(_)) => std::cmp::Ordering::Greater, - (InputAction::Unknown, InputAction::Unknown) => std::cmp::Ordering::Equal, - (InputAction::Unknown, _) => std::cmp::Ordering::Less, - (_, InputAction::Unknown) => std::cmp::Ordering::Greater, + (InputItem::Key(_), InputItem::Button(_)) => std::cmp::Ordering::Less, + (InputItem::Button(_), InputItem::Key(_)) => std::cmp::Ordering::Greater, } } } -impl PartialOrd for InputAction { +impl PartialOrd for InputItem { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Display for InputAction { +impl Display for InputItem { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self { - InputAction::Key(k) => write!(f, "{:?}", k), - InputAction::Button(b) => write!(f, "{:?}", b), - InputAction::Unknown => write!(f, "Unknown"), + InputItem::Key(k) => write!(f, "{:?}", k), + InputItem::Button(b) => write!(f, "{:?}", b), + } + } +} + +impl FromStr for InputAction { + type Err = String; + + fn from_str(s: &str) -> Result { + let parts = s.split('+'); + let mut items = BTreeSet::new(); + + for part in parts { + let part = part.trim(); + if let Ok(item) = InputItem::from_str(part) { + items.insert(item); + } else { + return Err(format!("Failed to parse InputAction from string: {}", s)); + } } + + Ok(InputAction(items)) } } +impl Display for InputAction { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let mut items: Vec = self.0.iter().map(|item| format!("{}", item)).collect(); + items.sort(); // Ensure consistent order + write!(f, "{}", items.join("+")) + } +} + +pub fn from_egui(key: egui::Key, modifiers: egui::Modifiers) -> InputAction { + let mut items = BTreeSet::new(); + + if modifiers.ctrl { + items.insert(InputItem::Key(Key::ControlLeft)); + } + if modifiers.alt { + items.insert(InputItem::Key(Key::Alt)); + } + if modifiers.shift { + items.insert(InputItem::Key(Key::ShiftLeft)); + } + if modifiers.command { + items.insert(InputItem::Key(Key::MetaLeft)); + } + + if let Ok(rdev_key) = map_egui_key_to_rdev_key(key) { + items.insert(InputItem::Key(rdev_key)); + } else { + error!("Failed to map egui::Key to rdev::Key: {:?}", key); + } + + InputAction(items) +} + #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct MousePosition { pub x: i32, @@ -119,7 +226,7 @@ pub struct MousePosition { pub struct SharedInputState { last_mouse_position: Mutex>, last_click_time: Mutex>, - keys_pressed: Mutex>, + keys_pressed: Mutex>, callbacks: Mutex>, } @@ -133,7 +240,7 @@ impl SharedInputState { } } - fn update_input_key_press(&self, input_key: InputAction) { + fn update_input_key_press(&self, input_key: InputItem) { if let Ok(mut keys) = self.keys_pressed.lock() { keys.insert(input_key); } else { @@ -141,7 +248,7 @@ impl SharedInputState { } } - fn update_input_key_release(&self, input_key: InputAction) { + fn update_input_key_release(&self, input_key: InputItem) { if let Ok(mut keys) = self.keys_pressed.lock() { keys.remove(&input_key); } else { @@ -158,7 +265,7 @@ impl SharedInputState { } fn update_button_press(&self, button: Button) { - self.update_input_key_press(InputAction::Button(button)); + self.update_input_key_press(InputItem::Button(button)); if let Ok(mut time) = self.last_click_time.lock() { *time = Some(Instant::now()); @@ -168,15 +275,15 @@ impl SharedInputState { } fn update_button_release(&self, button: Button) { - self.update_input_key_release(InputAction::Button(button)); + self.update_input_key_release(InputItem::Button(button)); } fn update_key_press(&self, key: Key) { - self.update_input_key_press(InputAction::Key(key)); + self.update_input_key_press(InputItem::Key(key)); } fn update_key_release(&self, key: Key) { - self.update_input_key_release(InputAction::Key(key)); + self.update_input_key_release(InputItem::Key(key)); } fn execute_callbacks(&self, event: &Event) { @@ -222,7 +329,7 @@ impl InputListener { } } - pub fn is_input_key_pressed(&self, input_key: InputAction) -> bool { + pub fn is_input_key_pressed(&self, input_key: InputItem) -> bool { match self.state.keys_pressed.lock() { Ok(guard) => guard.contains(&input_key), Err(e) => { @@ -233,14 +340,34 @@ impl InputListener { } pub fn is_key_pressed(&self, key: Key) -> bool { - self.is_input_key_pressed(InputAction::Key(key)) + self.is_input_key_pressed(InputItem::Key(key)) } pub fn is_button_pressed(&self, button: Button) -> bool { - self.is_input_key_pressed(InputAction::Button(button)) + self.is_input_key_pressed(InputItem::Button(button)) + } + + pub fn get_keys_pressed(&self) -> BTreeSet { + match self.state.keys_pressed.lock() { + Ok(guard) => guard.clone(), + Err(e) => { + error!("Failed to lock keys_pressed mutex: {}", e); + BTreeSet::new() + } + } + } + + pub fn is_input_action_pressed(&self, input_action: &InputAction) -> bool { + match self.state.keys_pressed.lock() { + Ok(guard) => input_action.0.is_subset(&guard), + Err(e) => { + error!("Failed to lock keys_pressed mutex: {}", e); + false + } + } } - pub fn get_keys_pressed(&self) -> BTreeSet { + pub fn get_items_pressed(&self) -> BTreeSet { match self.state.keys_pressed.lock() { Ok(guard) => guard.clone(), Err(e) => { @@ -306,11 +433,11 @@ mod tests { #[test] fn test_input_action_equality() { - let key_a = InputAction::Key(Key::KeyA); - let key_a2 = InputAction::Key(Key::KeyA); - let key_b = InputAction::Key(Key::KeyB); - let button_left = InputAction::Button(Button::Left); - let button_right = InputAction::Button(Button::Right); + let key_a = InputItem::Key(Key::KeyA); + let key_a2 = InputItem::Key(Key::KeyA); + let key_b = InputItem::Key(Key::KeyB); + let button_left = InputItem::Button(Button::Left); + let button_right = InputItem::Button(Button::Right); assert_eq!(key_a, key_a2); assert_ne!(key_a, key_b); @@ -321,10 +448,10 @@ mod tests { #[test] fn test_input_action_ordering() { let mut actions = vec![ - InputAction::Key(Key::KeyB), - InputAction::Button(Button::Left), - InputAction::Key(Key::KeyA), - InputAction::Button(Button::Right), + InputItem::Key(Key::KeyB), + InputItem::Button(Button::Left), + InputItem::Key(Key::KeyA), + InputItem::Button(Button::Right), ]; actions.sort(); @@ -332,18 +459,18 @@ mod tests { assert_eq!( actions, vec![ - InputAction::Key(Key::KeyA), - InputAction::Key(Key::KeyB), - InputAction::Button(Button::Left), - InputAction::Button(Button::Right), + InputItem::Key(Key::KeyA), + InputItem::Key(Key::KeyB), + InputItem::Button(Button::Left), + InputItem::Button(Button::Right), ] ); } #[test] fn test_input_action_display() { - let key = InputAction::Key(Key::KeyA); - let button = InputAction::Button(Button::Left); + let key = InputItem::Key(Key::KeyA); + let button = InputItem::Button(Button::Left); assert_eq!(format!("{}", key), "KeyA"); assert_eq!(format!("{}", button), "Left"); @@ -353,9 +480,9 @@ mod tests { fn test_input_action_hash() { use std::collections::hash_map::DefaultHasher; - let key_a1 = InputAction::Key(Key::KeyA); - let key_a2 = InputAction::Key(Key::KeyA); - let button_left = InputAction::Button(Button::Left); + let key_a1 = InputItem::Key(Key::KeyA); + let key_a2 = InputItem::Key(Key::KeyA); + let button_left = InputItem::Button(Button::Left); let mut hasher1 = DefaultHasher::new(); key_a1.hash(&mut hasher1); @@ -381,14 +508,14 @@ mod tests { state.update_key_press(Key::KeyA); { let keys_pressed = state.keys_pressed.lock().unwrap(); - assert!(keys_pressed.contains(&InputAction::Key(Key::KeyA))); + assert!(keys_pressed.contains(&InputItem::Key(Key::KeyA))); } // Simulate key release state.update_key_release(Key::KeyA); { let keys_pressed = state.keys_pressed.lock().unwrap(); - assert!(!keys_pressed.contains(&InputAction::Key(Key::KeyA))); + assert!(!keys_pressed.contains(&InputItem::Key(Key::KeyA))); } } @@ -400,14 +527,14 @@ mod tests { state.update_button_press(Button::Left); { let keys_pressed = state.keys_pressed.lock().unwrap(); - assert!(keys_pressed.contains(&InputAction::Button(Button::Left))); + assert!(keys_pressed.contains(&InputItem::Button(Button::Left))); } // Simulate button release state.update_button_release(Button::Left); { let keys_pressed = state.keys_pressed.lock().unwrap(); - assert!(!keys_pressed.contains(&InputAction::Button(Button::Left))); + assert!(!keys_pressed.contains(&InputItem::Button(Button::Left))); } } @@ -498,10 +625,9 @@ mod tests { listener.state.update_key_press(Key::KeyB); let keys_pressed = listener.get_keys_pressed(); - let expected_keys: BTreeSet<_> = - vec![InputAction::Key(Key::KeyA), InputAction::Key(Key::KeyB)] - .into_iter() - .collect(); + let expected_keys: BTreeSet<_> = vec![InputItem::Key(Key::KeyA), InputItem::Key(Key::KeyB)] + .into_iter() + .collect(); assert_eq!(keys_pressed, expected_keys); } diff --git a/src/shortcut.rs b/src/shortcut.rs index 5f8757d..289c888 100644 --- a/src/shortcut.rs +++ b/src/shortcut.rs @@ -1,20 +1,20 @@ -use crate::listener::{InputAction, InputListener}; +use crate::listener::{InputItem, InputListener}; use eframe::egui::TextBuffer; use std::collections::BTreeSet; use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; use std::str::FromStr; use std::sync::{Arc, Mutex}; -pub type ShortcutCallback = Arc) + Send + Sync + 'static>; +pub type ShortcutCallback = Arc) + Send + Sync + 'static>; #[derive(Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] pub struct KeyboardShortcut { - pub keys: BTreeSet, + pub keys: BTreeSet, display_name: String, } impl KeyboardShortcut { - pub fn new(keys: BTreeSet, display_name: String) -> Self { + pub fn new(keys: BTreeSet, display_name: String) -> Self { KeyboardShortcut { keys, display_name } } @@ -25,7 +25,7 @@ impl KeyboardShortcut { .fold(String::new(), |acc, key| acc + &format!("{} + ", key)); } - fn is_matched(&self, keys_pressed: &BTreeSet) -> bool { + fn is_matched(&self, keys_pressed: &BTreeSet) -> bool { self.keys.is_subset(keys_pressed) } } @@ -45,7 +45,7 @@ impl TextBuffer for KeyboardShortcut { if offset != char_index { continue; } - if let Ok(key) = InputAction::from_str(&c.to_string()) { + if let Ok(key) = InputItem::from_str(&c.to_string()) { new_keys.insert(key); } } @@ -112,7 +112,7 @@ impl ShortcutManager { pub fn add_shortcut(&self, action_name: String, shortcut: KeyboardShortcut, callback: F) where - F: Fn(BTreeSet) + Send + Sync + 'static, + F: Fn(BTreeSet) + Send + Sync + 'static, { let arc_callback: ShortcutCallback = Arc::new(callback); @@ -205,25 +205,23 @@ impl Default for ShortcutManager { #[cfg(test)] mod tests { use super::*; - use crate::listener::{InputAction, InputListener}; + use crate::listener::{InputItem, InputListener}; use rdev::Key; use std::sync::atomic::{AtomicBool, Ordering}; #[test] fn test_keyboard_shortcut_new() { - let keys: BTreeSet<_> = vec![InputAction::Key(Key::KeyA)].into_iter().collect(); + let keys: BTreeSet<_> = vec![InputItem::Key(Key::KeyA)].into_iter().collect(); let shortcut = KeyboardShortcut::new(keys.clone(), "TestAction".to_string()); assert_eq!(shortcut.keys, keys); } #[test] fn test_keyboard_shortcut_is_matched() { - let shortcut_keys: BTreeSet<_> = vec![ - InputAction::Key(Key::ControlLeft), - InputAction::Key(Key::KeyC), - ] - .into_iter() - .collect(); + let shortcut_keys: BTreeSet<_> = + vec![InputItem::Key(Key::ControlLeft), InputItem::Key(Key::KeyC)] + .into_iter() + .collect(); let shortcut = KeyboardShortcut::new(shortcut_keys.clone(), "TestAction".to_string()); // Test with matching keys_pressed @@ -232,24 +230,20 @@ mod tests { // Test with extra keys in keys_pressed let mut keys_pressed_extra = keys_pressed.clone(); - keys_pressed_extra.insert(InputAction::Key(Key::ShiftLeft)); + keys_pressed_extra.insert(InputItem::Key(Key::ShiftLeft)); assert!(shortcut.is_matched(&keys_pressed_extra)); // Test with missing keys in keys_pressed - let keys_pressed_missing: BTreeSet<_> = vec![InputAction::Key(Key::ControlLeft)] - .into_iter() - .collect(); + let keys_pressed_missing: BTreeSet<_> = + vec![InputItem::Key(Key::ControlLeft)].into_iter().collect(); assert!(!shortcut.is_matched(&keys_pressed_missing)); } #[test] fn test_keyboard_shortcut_display() { - let keys: BTreeSet<_> = vec![ - InputAction::Key(Key::ControlLeft), - InputAction::Key(Key::KeyA), - ] - .into_iter() - .collect(); + let keys: BTreeSet<_> = vec![InputItem::Key(Key::ControlLeft), InputItem::Key(Key::KeyA)] + .into_iter() + .collect(); let shortcut = KeyboardShortcut::new(keys, "TestAction".to_string()); let display_str = format!("{}", shortcut); assert!(display_str.contains("ControlLeft")); @@ -258,7 +252,7 @@ mod tests { #[test] fn test_keyboard_shortcut_callback_creation() { - let keys: BTreeSet<_> = vec![InputAction::Key(Key::KeyA)].into_iter().collect(); + let keys: BTreeSet<_> = vec![InputItem::Key(Key::KeyA)].into_iter().collect(); let shortcut = KeyboardShortcut::new(keys, "TestAction".to_string()); let callback_called = Arc::new(AtomicBool::new(false)); let callback_called_clone = Arc::clone(&callback_called); @@ -282,7 +276,7 @@ mod tests { fn test_shortcut_manager_add_shortcut() { let input_listener = InputListener::new(); let shortcut_manager = ShortcutManager::new(input_listener); - let keys: BTreeSet<_> = vec![InputAction::Key(Key::KeyA)].into_iter().collect(); + let keys: BTreeSet<_> = vec![InputItem::Key(Key::KeyA)].into_iter().collect(); let shortcut = KeyboardShortcut::new(keys.clone(), "TestAction".to_string()); shortcut_manager.add_shortcut(