Naive implementation of sending strings to the host

This commit is contained in:
Jakub Hlusička 2026-02-09 02:14:06 +01:00
parent 1e2d43a628
commit 3b364c64c2
4 changed files with 342 additions and 122 deletions

View file

@ -2,20 +2,26 @@ use core::alloc::Layout;
use core::fmt::Debug; use core::fmt::Debug;
use core::slice; use core::slice;
use alloc::alloc::Allocator;
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::collections::btree_map::BTreeMap; use alloc::collections::btree_map::{BTreeMap, Entry};
use alloc::string::String; use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::channel::Channel; use embassy_sync::channel::Channel;
use embassy_time::Instant; use embassy_time::Instant;
use esp_alloc::MemoryCapability; use esp_alloc::{EspHeap, MemoryCapability};
use itertools::Itertools;
use log::{debug, info, warn}; use log::{debug, info, warn};
use rmk::descriptor::KeyboardReport; use rmk::descriptor::KeyboardReport;
use rmk::futures::FutureExt;
use rmk::hid::Report; use rmk::hid::Report;
use rmk::types::action::{Action, KeyAction}; use rmk::types::action::{Action, KeyAction};
use rmk::{a, k, layer}; use rmk::{a, join_all, k, layer};
use slint::platform::Key; use slint::platform::Key;
use xkbcommon::xkb::{self, FeedResult, KeyDirection, Keysym, Status}; use static_cell::StaticCell;
use xkbcommon::xkb::{self, FeedResult, KeyDirection, Keysym, ModMask, Status};
use crate::matrix::{MATRIX_COLS, MATRIX_ROWS}; use crate::matrix::{MATRIX_COLS, MATRIX_ROWS};
use crate::util::DurationExt; use crate::util::DurationExt;
@ -25,6 +31,7 @@ use crate::{KEYBOARD_REPORT_PROXY, PSRAM_ALLOCATOR};
pub const NUM_LAYER: usize = 1; pub const NUM_LAYER: usize = 1;
pub static KEY_MESSAGE_CHANNEL: Channel<CriticalSectionRawMutex, KeyMessage, 16> = Channel::new(); pub static KEY_MESSAGE_CHANNEL: Channel<CriticalSectionRawMutex, KeyMessage, 16> = Channel::new();
pub static OUTPUT_STRING_CHANNEL: Channel<CriticalSectionRawMutex, String, 16> = Channel::new();
pub struct KeyMessage { pub struct KeyMessage {
pub keysym: Keysym, pub keysym: Keysym,
@ -83,13 +90,16 @@ pub fn create_hid_report_interceptor() -> impl Future<Output = ()> {
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
info!("Loading XKB keymap..."); info!("Loading XKB keymap...");
let instant_start = Instant::now(); let instant_start = Instant::now();
let keymap = xkb::Keymap::new_from_string( let keymap = Arc::new_in(
&context, xkb::Keymap::new_from_string(
keymap_string_buffer, &context,
xkb::KEYMAP_FORMAT_TEXT_V1, keymap_string_buffer,
xkb::KEYMAP_COMPILE_NO_FLAGS, xkb::KEYMAP_FORMAT_TEXT_V1,
) xkb::KEYMAP_COMPILE_NO_FLAGS,
.unwrap(); )
.unwrap(),
&PSRAM_ALLOCATOR,
);
let duration = Instant::now().duration_since(instant_start); let duration = Instant::now().duration_since(instant_start);
info!( info!(
"XKB keymap loaded successfully! Took {} seconds.", "XKB keymap loaded successfully! Took {} seconds.",
@ -97,19 +107,69 @@ pub fn create_hid_report_interceptor() -> impl Future<Output = ()> {
); );
info!("Loading XKB compose map..."); info!("Loading XKB compose map...");
let instant_start = Instant::now(); let instant_start = Instant::now();
let compose_table = xkb::compose::Table::new_from_buffer( let compose_table = Arc::new_in(
&context, xkb::compose::Table::new_from_buffer(
COMPOSE_MAP_STRING, &context,
COMPOSE_MAP_LOCALE, COMPOSE_MAP_STRING,
xkb::compose::FORMAT_TEXT_V1, COMPOSE_MAP_LOCALE,
xkb::compose::COMPILE_NO_FLAGS, xkb::compose::FORMAT_TEXT_V1,
) xkb::compose::COMPILE_NO_FLAGS,
.unwrap(); )
.unwrap(),
&PSRAM_ALLOCATOR,
);
let duration = Instant::now().duration_since(instant_start); let duration = Instant::now().duration_since(instant_start);
info!( info!(
"XKB compose map loaded successfully! Took {} seconds.", "XKB compose map loaded successfully! Took {} seconds.",
duration.display_as_secs() duration.display_as_secs()
); );
async move {
join_all![
send_keycodes_to_slint(keymap.clone(), compose_table.clone()),
send_strings_to_host(keymap, compose_table)
]
.await;
}
}
async fn send_strings_to_host<A: Allocator>(
keymap: Arc<xkb::Keymap, A>,
compose_table: Arc<xkb::compose::Table, A>,
) {
loop {
let string = OUTPUT_STRING_CHANNEL.receive().await;
let keycodes = string_to_hid_keycodes(&keymap, &compose_table, &string);
warn!("keycodes for {string:?}: {keycodes:02x?}");
if let Ok(keycodes) = keycodes {
for keycode_chunk in keycodes.chunks(6) {
rmk::channel::KEYBOARD_REPORT_RECEIVER
.send(Report::KeyboardReport(KeyboardReport {
modifier: Default::default(), // TODO
reserved: Default::default(), // TODO
leds: Default::default(),
keycodes: {
// Send multiple keycodes at the same time.
let mut keycodes = [0; _];
keycodes[0..keycode_chunk.len()].copy_from_slice(keycode_chunk);
keycodes
},
}))
.await;
rmk::channel::KEYBOARD_REPORT_RECEIVER
.send(Report::KeyboardReport(KeyboardReport::default()))
.await;
}
}
}
}
async fn send_keycodes_to_slint<A: Allocator>(
keymap: Arc<xkb::Keymap, A>,
compose_table: Arc<xkb::compose::Table, A>,
) {
let mut state = xkb::State::new(&keymap); let mut state = xkb::State::new(&keymap);
let mut previous_state = KeyboardReport::default(); let mut previous_state = KeyboardReport::default();
let mut compose_state = xkb::compose::State::new(&compose_table, xkb::compose::STATE_NO_FLAGS); let mut compose_state = xkb::compose::State::new(&compose_table, xkb::compose::STATE_NO_FLAGS);
@ -117,130 +177,207 @@ pub fn create_hid_report_interceptor() -> impl Future<Output = ()> {
// This is a map from the basic keysyms (not a composed ones) to the string that should be produced. // This is a map from the basic keysyms (not a composed ones) to the string that should be produced.
let mut pressed_keys_to_strings = BTreeMap::<Keysym, Option<String>>::new(); let mut pressed_keys_to_strings = BTreeMap::<Keysym, Option<String>>::new();
async move { loop {
loop { let report = KEYBOARD_REPORT_PROXY.receive().await;
let report = KEYBOARD_REPORT_PROXY.receive().await;
if let Report::KeyboardReport(report) = &report { if let Report::KeyboardReport(report) = &report {
const KEYCODES_LEN_MODIFIERS: usize = 8; const KEYCODES_LEN_MODIFIERS: usize = 8;
const KEYCODES_LEN_REGULAR: usize = 6; const KEYCODES_LEN_REGULAR: usize = 6;
const KEYCODES_LEN: usize = KEYCODES_LEN_MODIFIERS + KEYCODES_LEN_REGULAR; const KEYCODES_LEN: usize = KEYCODES_LEN_MODIFIERS + KEYCODES_LEN_REGULAR;
let mut pressed_keys = rmk::heapless::Vec::<u8, KEYCODES_LEN>::new(); let mut pressed_keys = rmk::heapless::Vec::<u8, KEYCODES_LEN>::new();
let mut released_keys = rmk::heapless::Vec::<u8, KEYCODES_LEN>::new(); let mut released_keys = rmk::heapless::Vec::<u8, KEYCODES_LEN>::new();
let pressed_mods_bits = !previous_state.modifier & report.modifier; let pressed_mods_bits = !previous_state.modifier & report.modifier;
let released_mods_bits = previous_state.modifier & !report.modifier; let released_mods_bits = previous_state.modifier & !report.modifier;
for index in 0..KEYCODES_LEN_MODIFIERS { for index in 0..KEYCODES_LEN_MODIFIERS {
const USB_HID_LEFT_CTRL: u8 = 0xE0; const USB_HID_LEFT_CTRL: u8 = 0xE0;
let mod_bit = 1_u8 << index; let mod_bit = 1_u8 << index;
let mod_keycode = USB_HID_LEFT_CTRL + index as u8; let mod_keycode = USB_HID_LEFT_CTRL + index as u8;
if pressed_mods_bits & mod_bit != 0 { if pressed_mods_bits & mod_bit != 0 {
pressed_keys.push(mod_keycode).unwrap(); pressed_keys.push(mod_keycode).unwrap();
}
if released_mods_bits & mod_bit != 0 {
released_keys.push(mod_keycode).unwrap();
}
} }
// TODO: This currently depends on pressed keys not changing position in the array. if released_mods_bits & mod_bit != 0 {
// Should be made independent of that. released_keys.push(mod_keycode).unwrap();
for (&keycode_old, &keycode_new) in }
core::iter::zip(&previous_state.keycodes, &report.keycodes) }
// TODO: This currently depends on pressed keys not changing position in the array.
// Should be made independent of that.
for (&keycode_old, &keycode_new) in
core::iter::zip(&previous_state.keycodes, &report.keycodes)
{
if keycode_old == keycode_new {
continue;
}
if keycode_old != 0 {
released_keys.push(keycode_old).unwrap();
}
if keycode_new != 0 {
pressed_keys.push(keycode_new).unwrap();
}
}
previous_state = *report;
for keycode in released_keys {
debug!("Release: 0x{:02x} ({})", keycode, keycode);
let keycode_xkb = hid_to_xkb_keycode(keycode);
state.update_key(keycode_xkb, KeyDirection::Up);
let keysym = state.key_get_one_sym(keycode_xkb);
let string = pressed_keys_to_strings.remove(&keysym).unwrap_or_else(|| {
warn!("Could not determine the string of a released key: keysym={keysym:?} pressed_keys_to_strings={pressed_keys_to_strings:?}");
None
});
KEY_MESSAGE_CHANNEL
.send(KeyMessage {
keysym,
string,
direction: KeyDirection::Up,
})
.await;
}
for keycode in pressed_keys {
debug!("Pressed: 0x{:02x} ({})", keycode, keycode);
let keycode_xkb = hid_to_xkb_keycode(keycode);
let sym = state.key_get_one_sym(keycode_xkb);
let result: Option<(Keysym, Keysym, Option<String>)> = match compose_state.feed(sym)
{ {
if keycode_old == keycode_new { FeedResult::Ignored => {
continue; let string = state.key_get_utf8(keycode_xkb);
Some((sym, sym, Some(string)))
} }
FeedResult::Accepted => {
if keycode_old != 0 { let status = compose_state.status();
released_keys.push(keycode_old).unwrap(); debug!("Compose status: {status:?}");
match status {
Status::Nothing => {
let string = state.key_get_utf8(keycode_xkb);
Some((sym, sym, Some(string)))
}
Status::Composing => None,
Status::Composed => {
let composed_sym = compose_state.keysym().unwrap_or_default();
let string = compose_state.utf8().unwrap_or_default();
Some((sym, composed_sym, Some(string)))
}
Status::Cancelled => None,
}
} }
};
if keycode_new != 0 { if let Some((basic_keysym, composed_keysym, string)) = result {
pressed_keys.push(keycode_new).unwrap(); // Change `Some("")` into `None`.
} let string = string.filter(|string| !string.is_empty());
}
previous_state = *report; info!(
"Basic keysym: {basic_keysym:?}, composed keysym: {composed_keysym:?}, string: {:?}",
string.as_ref()
);
for keycode in released_keys { pressed_keys_to_strings.insert(basic_keysym, string.clone());
debug!("Release: 0x{:02x} ({})", keycode, keycode);
let keycode_xkb = into_xkb_keycode(keycode);
state.update_key(keycode_xkb, KeyDirection::Up);
let keysym = state.key_get_one_sym(keycode_xkb);
let string = pressed_keys_to_strings.remove(&keysym).unwrap_or_else(|| {
warn!("Could not determine the string of a released key: keysym={keysym:?} pressed_keys_to_strings={pressed_keys_to_strings:?}");
None
});
KEY_MESSAGE_CHANNEL KEY_MESSAGE_CHANNEL
.send(KeyMessage { .send(KeyMessage {
keysym, keysym: composed_keysym,
string, string,
direction: KeyDirection::Up, direction: KeyDirection::Down,
}) })
.await; .await;
} state.update_key(keycode_xkb, KeyDirection::Down);
for keycode in pressed_keys {
debug!("Pressed: 0x{:02x} ({})", keycode, keycode);
let keycode_xkb = into_xkb_keycode(keycode);
let sym = state.key_get_one_sym(keycode_xkb);
let result: Option<(Keysym, Keysym, Option<String>)> = match compose_state
.feed(sym)
{
FeedResult::Ignored => {
let string = state.key_get_utf8(keycode_xkb);
Some((sym, sym, Some(string)))
}
FeedResult::Accepted => {
let status = compose_state.status();
debug!("Compose status: {status:?}");
match status {
Status::Nothing => {
let string = state.key_get_utf8(keycode_xkb);
Some((sym, sym, Some(string)))
}
Status::Composing => None,
Status::Composed => {
let composed_sym = compose_state.keysym().unwrap_or_default();
let string = compose_state.utf8().unwrap_or_default();
Some((sym, composed_sym, Some(string)))
}
Status::Cancelled => None,
}
}
};
if let Some((basic_keysym, composed_keysym, string)) = result {
// Change `Some("")` into `None`.
let string = string.filter(|string| !string.is_empty());
info!(
"Basic keysym: {basic_keysym:?}, composed keysym: {composed_keysym:?}, string: {:?}",
string.as_ref()
);
pressed_keys_to_strings.insert(basic_keysym, string.clone());
KEY_MESSAGE_CHANNEL
.send(KeyMessage {
keysym: composed_keysym,
string,
direction: KeyDirection::Down,
})
.await;
state.update_key(keycode_xkb, KeyDirection::Down);
}
} }
} }
} }
} }
} }
fn into_xkb_keycode(rmk_keycode: u8) -> xkb::Keycode { pub fn keysym_to_xkb_keycode() {}
/// Based on https://github.com/xkbcommon/libxkbcommon/blob/6c67e3d41d3215ab1edd4406de215c7bf1f20c74/tools/how-to-type.c#L389
/// Fields reordered for `Ord` based on https://github.com/xkbcommon/libxkbcommon/blob/6c67e3d41d3215ab1edd4406de215c7bf1f20c74/tools/how-to-type.c#L404
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct KeysymEntry {
layout: xkb::LayoutIndex,
level: xkb::LevelIndex,
keycode: xkb::Keycode,
mask: xkb::ModMask,
}
type KeysymEntries = Vec<KeysymEntry, &'static EspHeap>;
type KeysymMap = BTreeMap<Keysym, KeysymEntries, &'static EspHeap>;
/// Based on https://github.com/xkbcommon/libxkbcommon/blob/6c67e3d41d3215ab1edd4406de215c7bf1f20c74/tools/how-to-type.c#L434
fn lookup_keysym_entries(
entries: &mut KeysymMap,
keysym: xkb::Keysym,
insert: bool,
) -> Option<&mut KeysymEntries> {
match entries.entry(keysym) {
Entry::Occupied(occupied) => Some(occupied.into_mut()),
Entry::Vacant(vacant) if insert => Some(vacant.insert(Vec::new_in(&PSRAM_ALLOCATOR))),
Entry::Vacant(_) => None,
}
}
pub fn string_to_hid_keycodes(
keymap: &xkb::Keymap,
compose_table: &xkb::compose::Table,
string: &str,
) -> Result<Vec<u8, &'static EspHeap>, char> {
let mut hid_keycodes = Vec::new_in(&PSRAM_ALLOCATOR);
for character in string.chars() {
let keysym = xkbcommon::xkb::utf32_to_keysym(character as u32);
let mut found_keycode = None;
keymap.key_for_each(|keymap, keycode| {
if found_keycode.is_some() {
return;
}
for layout_index in 0..keymap.num_layouts_for_key(keycode) {
for level_index in 0..keymap.num_levels_for_key(keycode, layout_index) {
let [current_keysym] =
keymap.key_get_syms_by_level(keycode, layout_index, level_index)
else {
// Multi-keysym levels are currently not handled.
continue;
};
if current_keysym == &keysym {
let mut masks: [ModMask; 32 /* TODO: Re-evaluate the appropriate size */] = Default::default();
let masks_len = keymap.key_get_mods_for_level(
keycode,
layout_index,
level_index,
&mut masks,
);
let masks = &mut masks[0..masks_len];
warn!("string_to_hid_keycodes: char = {character:?}, sym = {keysym:?}, code = {keycode:?}, layout = {layout_index:?}, level = {level_index:?}, masks = {masks:?}");
found_keycode = Some(keycode);
}
}
}
});
let Some(found_keycode) = found_keycode else {
return Err(character);
};
let hid_keycode = xkb_to_hid_keycode(found_keycode);
hid_keycodes.push(hid_keycode);
}
Ok(hid_keycodes)
}
const fn hid_to_xkb_keycode_inner(rmk_keycode: u8) -> u8 {
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/hid/hid-input.c?id=refs/tags/v6.18#n27 // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/hid/hid-input.c?id=refs/tags/v6.18#n27
const UNK: u8 = 240; const UNK: u8 = 240;
#[rustfmt::skip] #[rustfmt::skip]
@ -267,7 +404,48 @@ fn into_xkb_keycode(rmk_keycode: u8) -> xkb::Keycode {
// TODO: The combination of these two operations should be precomputed // TODO: The combination of these two operations should be precomputed
// in a const expr into a single look-up table. // in a const expr into a single look-up table.
xkb::Keycode::new((HID_KEYBOARD[rmk_keycode as usize] + MIN_KEYCODE) as u32) HID_KEYBOARD[rmk_keycode as usize] + MIN_KEYCODE
// xkb::Keycode::new((HID_KEYBOARD[rmk_keycode as usize] + MIN_KEYCODE) as u32)
}
pub const fn hid_to_xkb_keycode(hid_keycode: u8) -> xkb::Keycode {
const HID_TO_XKB: [u8; 256] = {
let mut hid_to_xkb = [0_u8; _];
let mut hid: u8 = 0;
loop {
hid_to_xkb[hid as usize] = hid_to_xkb_keycode_inner(hid);
let overflow;
(hid, overflow) = hid.overflowing_add(1);
if overflow {
break;
}
}
hid_to_xkb
};
xkb::Keycode::new(HID_TO_XKB[hid_keycode as usize] as u32)
}
pub const fn xkb_to_hid_keycode(xkb_keycode: xkb::Keycode) -> u8 {
const XKB_TO_HID: [u8; 256] = {
let mut xkb_to_hid = [0_u8; _];
let mut hid: u8 = 0;
loop {
xkb_to_hid[hid_to_xkb_keycode_inner(hid as u8) as usize] = hid;
let overflow;
(hid, overflow) = hid.overflowing_add(1);
if overflow {
break;
}
}
xkb_to_hid
};
XKB_TO_HID[xkb_keycode.raw() as usize]
} }
pub trait TryFromKeysym: Sized { pub trait TryFromKeysym: Sized {

View file

@ -10,12 +10,14 @@ use alloc::{
rc::Rc, rc::Rc,
string::{String, ToString}, string::{String, ToString},
vec, vec,
vec::Vec,
}; };
use embassy_time::Instant; use embassy_time::Instant;
use hex::FromHexError; use hex::FromHexError;
use i_slint_core::model::{ModelChangeListener, ModelChangeListenerContainer}; use i_slint_core::model::{ModelChangeListener, ModelChangeListenerContainer};
use log::{error, info, warn}; use log::{error, info, warn};
use password_hash::Key; use password_hash::Key;
use rmk::futures::TryFutureExt;
use slint::{ use slint::{
Model, ModelExt, ModelNotify, ModelRc, ModelTracker, SharedString, StandardListViewItem, Model, ModelExt, ModelNotify, ModelRc, ModelTracker, SharedString, StandardListViewItem,
VecModel, VecModel,
@ -30,10 +32,11 @@ use crate::FRAME_DURATION_MIN;
use crate::{ use crate::{
PSRAM_ALLOCATOR, SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER, PSRAM_ALLOCATOR, SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER,
db::{ db::{
AcidDatabase, DbKey, DbPathSpectreUserSites, DbPathSpectreUsers, PartitionAcid, AcidDatabase, DbKey, DbPathSpectreUserSite, DbPathSpectreUserSites, DbPathSpectreUsers,
ReadTransactionExt, PartitionAcid, ReadTransactionExt,
}, },
ffi::{alloc::__spre_free, crypto::ACTIVE_ENCRYPTED_USER_KEY}, ffi::{alloc::__spre_free, crypto::ACTIVE_ENCRYPTED_USER_KEY},
keymap::OUTPUT_STRING_CHANNEL,
ui::{ ui::{
backend::SlintBackend, backend::SlintBackend,
messages::{ messages::{
@ -189,6 +192,13 @@ impl State {
} }
}); });
main.on_login_test_string_accepted(|string| {
slint::spawn_local(async move {
OUTPUT_STRING_CHANNEL.send(string.to_string()).await;
})
.unwrap();
});
main.on_users_edit_user({ main.on_users_edit_user({
let state = state.clone(); let state = state.clone();
move |username, new| { move |username, new| {
@ -754,6 +764,32 @@ impl AppViewTrait for StateUserSites {
spectre_derive_site_password(&user_sites.user_key, &site_name_c); spectre_derive_site_password(&user_sites.user_key, &site_name_c);
warn!("Site password: {site_password:?}"); warn!("Site password: {site_password:?}");
for character in site_password.chars() {
let keysym = xkbcommon::xkb::utf32_to_keysym(character as u32);
}
slint::spawn_local({
let username = user_sites.username.clone();
let db = state.db.clone();
async move {
let mut write = db.write_transaction().await;
let key = DbKey::new(DbPathSpectreUserSite {
user_sites: DbPathSpectreUserSites {
username: Cow::Borrowed(&username),
},
site: Cow::Borrowed(&site_name),
});
let site = SpectreSiteConfig::default();
let site_bytes =
postcard::to_extend(&site, Vec::<u8, _>::new_in(&PSRAM_ALLOCATOR))
.unwrap();
write.write(&key, &site_bytes).await.unwrap();
write.commit().await.unwrap();
}
})
.unwrap();
} }
_ => (), _ => (),
} }

View file

@ -8,6 +8,7 @@ export component LoginView inherits VerticalLayout {
in property <[string]> usernames <=> combo_box_username.model; in property <[string]> usernames <=> combo_box_username.model;
callback users_clicked(); callback users_clicked();
callback pw_accepted(int, string, string); callback pw_accepted(int, string, string);
callback test_string_accepted <=> line_edit_test.accepted;
Rectangle { } Rectangle { }
HorizontalLayout { HorizontalLayout {
@ -39,5 +40,9 @@ export component LoginView inherits VerticalLayout {
} }
} }
line_edit_test := LineEdit {
placeholder-text: "Text to send to host.";
}
Rectangle { } Rectangle { }
} }

View file

@ -40,6 +40,7 @@ export component AppWindow inherits Window {
// Login View // Login View
in property <[string]> login_usernames <=> login_view.usernames; in property <[string]> login_usernames <=> login_view.usernames;
callback login_pw_accepted <=> login_view.pw_accepted; callback login_pw_accepted <=> login_view.pw_accepted;
callback login_test_string_accepted <=> login_view.test_string_accepted;
// Users View // Users View
callback users_edit_user <=> users_view.edit_user; callback users_edit_user <=> users_view.edit_user;
// User Edit View // User Edit View