First draft of macOS ui layer

This commit is contained in:
Federico Terzi 2021-02-08 21:13:33 +01:00
parent bee2001e28
commit 7d357149ff
11 changed files with 638 additions and 48 deletions

View File

@ -59,10 +59,7 @@ fn cc_config() {
#[cfg(target_os = "macos")]
fn cc_config() {
println!("cargo:rustc-link-lib=dylib=c++");
println!("cargo:rustc-link-lib=static=macbridge");
println!("cargo:rustc-link-lib=framework=Cocoa");
println!("cargo:rustc-link-lib=framework=IOKit");
// TODO
}
fn main() {

View File

@ -16,6 +16,9 @@ thiserror = "1.0.23"
widestring = "0.4.3"
lazycell = "1.3.0"
[target.'cfg(target_os="macos")'.dependencies]
lazycell = "1.3.0"
[target.'cfg(target_os="linux")'.dependencies]
notify-rust = "4.2.2"

View File

@ -40,17 +40,24 @@ fn cc_config() {
#[cfg(target_os = "linux")]
fn cc_config() {
// println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
// println!("cargo:rustc-link-lib=static=linuxbridge");
// println!("cargo:rustc-link-lib=dylib=X11");
// println!("cargo:rustc-link-lib=dylib=Xtst");
// println!("cargo:rustc-link-lib=dylib=xdo");
// Nothing to link on linux
}
#[cfg(target_os = "macos")]
fn cc_config() {
println!("cargo:rerun-if-changed=src/mac/native.mm");
println!("cargo:rerun-if-changed=src/mac/native.h");
println!("cargo:rerun-if-changed=src/mac/AppDelegate.mm");
println!("cargo:rerun-if-changed=src/mac/AppDelegate.h");
cc::Build::new()
.cpp(true)
.include("src/mac/native.h")
.include("src/mac/AppDelegate.h")
.file("src/mac/native.mm")
.file("src/mac/AppDelegate.mm")
.compile("espansoui");
println!("cargo:rustc-link-lib=dylib=c++");
println!("cargo:rustc-link-lib=static=macbridge");
println!("cargo:rustc-link-lib=static=espansoui");
println!("cargo:rustc-link-lib=framework=Cocoa");
println!("cargo:rustc-link-lib=framework=IOKit");
}

View File

@ -7,3 +7,6 @@ pub mod win32;
#[cfg(target_os = "linux")]
pub mod linux;
#[cfg(target_os = "macos")]
pub mod mac;

View File

@ -0,0 +1,39 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>
#include "native.h"
@interface AppDelegate : NSObject <NSApplicationDelegate, NSUserNotificationCenterDelegate> {
@public NSStatusItem *statusItem;
@public UIOptions options;
@public void *rust_instance;
@public EventCallback event_callback;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
- (void) setIcon: (int32_t) iconIndex;
- (void) popupMenu: (NSString *) payload;
- (void) showNotification: (NSString *) message withDelay:(double) delay;
- (IBAction) statusIconClick: (id) sender;
- (IBAction) contextMenuClick: (id) sender;
@end

View File

@ -0,0 +1,134 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#import "AppDelegate.h"
void addSeparatorMenu(NSMenu * parent);
void addSingleMenu(NSMenu * parent, id item);
void addSubMenu(NSMenu * parent, NSArray * items);
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
if (options.show_icon) {
statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
[self setIcon: 0];
}
[[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self];
}
- (void) setIcon: (int32_t)iconIndex {
if (options.show_icon) {
char * iconPath = options.icon_paths[iconIndex];
NSString *nsIconPath = [NSString stringWithUTF8String:iconPath];
NSImage *statusImage = [[NSImage alloc] initWithContentsOfFile:nsIconPath];
[statusImage setTemplate:YES];
[statusItem.button setImage:statusImage];
[statusItem setHighlightMode:YES];
[statusItem.button setAction:@selector(statusIconClick:)];
[statusItem.button setTarget:self];
}
}
- (IBAction) statusIconClick: (id) sender {
UIEvent event = {};
event.event_type = UI_EVENT_TYPE_ICON_CLICK;
if (event_callback && rust_instance) {
event_callback(rust_instance, event);
}
}
- (void) popupMenu: (NSString *) payload {
NSError *jsonError;
NSData *data = [payload dataUsingEncoding:NSUTF8StringEncoding];
NSArray *jsonMenuItems = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
NSMenu *menu = [[NSMenu alloc] initWithTitle:@"Espanso"];
addSubMenu(menu, jsonMenuItems);
[statusItem popUpStatusItemMenu: menu];
}
- (IBAction) contextMenuClick: (id) sender {
NSInteger itemId = [[sender valueForKey:@"tag"] integerValue];
UIEvent event = {};
event.event_type = UI_EVENT_TYPE_CONTEXT_MENU_CLICK;
event.context_menu_id = (uint32_t) [itemId intValue];
if (event_callback && rust_instance) {
event_callback(rust_instance, event);
}
}
- (void) showNotification: (NSString *) message withDelay: (double) delay {
NSUserNotification *notification = [[NSUserNotification alloc] init];
notification.title = @"Espanso";
notification.informativeText = message;
notification.soundName = nil;
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification];
[[NSUserNotificationCenter defaultUserNotificationCenter] performSelector:@selector(removeDeliveredNotification:) withObject:notification afterDelay:delay];
}
@end
// Menu utility methods
void addSeparatorMenu(NSMenu * parent)
{
[parent addItem: [NSMenuItem separatorItem]];
}
void addSingleMenu(NSMenu * parent, id item)
{
id label = [item objectForKey:@"label"];
id raw_id = [item objectForKey:@"raw_id"];
if (label == nil || raw_id == nil)
{
return;
}
NSMenuItem *newMenu = [[NSMenuItem alloc] initWithTitle:label action:@selector(contextMenuClick:) keyEquivalent:@""];
[newMenu setTag:(NSInteger)raw_id];
[parent addItem: newMenu];
}
void addSubMenu(NSMenu * parent, NSArray * items)
{
for (id item in items) {
id type = [item objectForKey:@"type"];
if ([type isEqualToString:@"simple"])
{
addSingleMenu(parent, item);
}
else if ([type isEqualToString:@"separator"])
{
addSeparatorMenu(parent);
}
else if ([type isEqualToString:@"sub"])
{
NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:[item objectForKey:@"label"] action:nil keyEquivalent:@""];
NSMenu *subMenu = [[NSMenu alloc] init];
[parent addItem: menuItem];
addSubMenu(subMenu, [item objectForKey:@"items"]);
[menuItem setSubmenu: subMenu];
}
}
}

241
espanso-ui/src/mac/mod.rs Normal file
View File

@ -0,0 +1,241 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::{cmp::min, collections::HashMap, ffi::{CString}, os::raw::c_char, thread::ThreadId};
use lazycell::LazyCell;
use log::{error, trace};
use crate::{event::UIEvent, icons::TrayIcon, menu::Menu};
// IMPORTANT: if you change these, also edit the native.h file.
const MAX_FILE_PATH: usize = 1024;
const MAX_ICON_COUNT: usize = 3;
const UI_EVENT_TYPE_ICON_CLICK: i32 = 1;
const UI_EVENT_TYPE_CONTEXT_MENU_CLICK: i32 = 2;
// Take a look at the native.h header file for an explanation of the fields
#[repr(C)]
pub struct RawUIOptions {
pub show_icon: i32,
pub icon_paths: [[u8; MAX_FILE_PATH]; MAX_ICON_COUNT],
pub icon_paths_count: i32,
}
// Take a look at the native.h header file for an explanation of the fields
#[repr(C)]
pub struct RawUIEvent {
pub event_type: i32,
pub context_menu_id: u32,
}
#[allow(improper_ctypes)]
#[link(name = "espansoui", kind = "static")]
extern "C" {
pub fn ui_initialize(
_self: *const MacEventLoop,
options: RawUIOptions,
);
pub fn ui_eventloop(
event_callback: extern "C" fn(_self: *mut MacEventLoop, event: RawUIEvent),
) -> i32;
pub fn ui_update_tray_icon(index: i32);
pub fn ui_show_notification(message: *const c_char, delay: f64);
pub fn ui_show_context_menu(payload: *const c_char);
}
pub struct MacUIOptions<'a> {
pub show_icon: bool,
pub icon_paths: &'a Vec<(TrayIcon, String)>,
}
pub fn create(options: MacUIOptions) -> (MacRemote, MacEventLoop) {
// Validate icons
if options.icon_paths.len() > MAX_ICON_COUNT {
panic!("MacOS UI received too many icon paths, please increase the MAX_ICON_COUNT constant to support more");
}
// Convert the icon paths to the internal representation
let mut icon_indexes: HashMap<TrayIcon, usize> = HashMap::new();
let mut icons = Vec::new();
for (index, (tray_icon, path)) in options.icon_paths.iter().enumerate() {
icon_indexes.insert(tray_icon.clone(), index);
icons.push(path.clone());
}
let eventloop = MacEventLoop::new(
icons,
options.show_icon,
);
let remote = MacRemote::new(icon_indexes);
(remote, eventloop)
}
pub type MacUIEventCallback = Box<dyn Fn(UIEvent)>;
pub struct MacEventLoop {
show_icon: bool,
icons: Vec<String>,
// Internal
_event_callback: LazyCell<MacUIEventCallback>,
_init_thread_id: LazyCell<ThreadId>,
}
impl MacEventLoop {
pub(crate) fn new(
icons: Vec<String>,
show_icon: bool,
) -> Self {
Self {
icons,
show_icon,
_event_callback: LazyCell::new(),
_init_thread_id: LazyCell::new(),
}
}
pub fn initialize(&mut self) {
// Convert the icon paths to the raw representation
let mut icon_paths: [[u8; MAX_FILE_PATH]; MAX_ICON_COUNT] =
[[0; MAX_FILE_PATH]; MAX_ICON_COUNT];
for (i, icon_path) in icon_paths.iter_mut().enumerate().take(self.icons.len()) {
let c_path = CString::new(self.icons[i].clone()).expect("unable to create CString for UI tray icon path");
let len = min(c_path.as_bytes().len(), MAX_FILE_PATH - 1);
icon_path[0..len].clone_from_slice(&c_path.as_bytes()[..len]);
}
let options = RawUIOptions {
show_icon: if self.show_icon { 1 } else { 0 },
icon_paths,
icon_paths_count: self.icons.len() as i32,
};
unsafe { ui_initialize(self as *const MacEventLoop, options) };
// Make sure the run() method is called in the same thread as initialize()
self
._init_thread_id
.fill(std::thread::current().id())
.expect("Unable to set initialization thread id");
}
pub fn run(&self, event_callback: MacUIEventCallback) {
// Make sure the run() method is called in the same thread as initialize()
if let Some(init_id) = self._init_thread_id.borrow() {
if init_id != &std::thread::current().id() {
panic!("MacEventLoop run() and initialize() methods should be called in the same thread");
}
}
if self._event_callback.fill(event_callback).is_err() {
panic!("Unable to set MacEventLoop callback");
}
extern "C" fn callback(_self: *mut MacEventLoop, event: RawUIEvent) {
if let Some(callback) = unsafe { (*_self)._event_callback.borrow() } {
let event: Option<UIEvent> = event.into();
if let Some(event) = event {
callback(event)
} else {
trace!("Unable to convert raw event to input event");
}
}
}
let error_code = unsafe { ui_eventloop(callback) };
if error_code <= 0 {
panic!("MacEventLoop exited with <= 0 code")
}
}
}
pub struct MacRemote {
// Maps icon name to their index
icon_indexes: HashMap<TrayIcon, usize>,
}
impl MacRemote {
pub(crate) fn new(
icon_indexes: HashMap<TrayIcon, usize>,
) -> Self {
Self {
icon_indexes,
}
}
pub fn update_tray_icon(&self, icon: TrayIcon) {
if let Some(index) = self.icon_indexes.get(&icon) {
unsafe { ui_update_tray_icon((*index) as i32) }
} else {
error!("Unable to update tray icon, invalid icon id");
}
}
pub fn show_notification(&self, message: &str) {
let c_string = CString::new(message);
match c_string {
Ok(message) => unsafe { ui_show_notification(message.as_ptr(), 3.0) },
Err(error) => {
error!(
"Unable to show notification {}",
error
);
}
}
}
pub fn show_context_menu(&self, menu: &Menu) {
match menu.to_json() {
Ok(payload) => {
let c_string = CString::new(payload);
match c_string {
Ok(c_string) => unsafe { ui_show_context_menu(c_string.as_ptr()) },
Err(error) => error!(
"Unable to show context menu, impossible to convert payload to c_string: {}",
error
),
}
}
Err(error) => {
error!("Unable to show context menu, {}", error);
}
}
}
}
#[allow(clippy::single_match)] // TODO: remove after another match is used
impl From<RawUIEvent> for Option<UIEvent> {
fn from(raw: RawUIEvent) -> Option<UIEvent> {
match raw.event_type {
UI_EVENT_TYPE_ICON_CLICK => {
return Some(UIEvent::TrayIconClick);
}
UI_EVENT_TYPE_CONTEXT_MENU_CLICK => {
return Some(UIEvent::ContextMenuClick(raw.context_menu_id));
}
_ => {}
}
None
}
}

View File

@ -0,0 +1,73 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef ESPANSO_UI_H
#define ESPANSO_UI_H
#include <stdint.h>
// Explicitly define this constant as we need to use it from the Rust side
#define MAX_FILE_PATH 1024
#define MAX_ICON_COUNT 3
#define UI_EVENT_TYPE_ICON_CLICK 1
#define UI_EVENT_TYPE_CONTEXT_MENU_CLICK 2
typedef struct {
int32_t show_icon;
char icon_paths[MAX_ICON_COUNT][MAX_FILE_PATH];
int32_t icon_paths_count;
} UIOptions;
typedef struct {
int32_t event_type;
uint32_t context_menu_id;
} UIEvent;
typedef void (*EventCallback)(void * self, UIEvent data);
typedef struct
{
UIOptions options;
// Rust interop
void *rust_instance;
EventCallback event_callback;
} UIVariables;
// Initialize the Application delegate.
extern "C" void ui_initialize(void * self, UIOptions options);
// Run the event loop. Blocking call.
extern "C" int32_t ui_eventloop(EventCallback callback);
// Updates the tray icon to the given one. The method accepts an index that refers to
// the icon within the UIOptions.icon_paths array.
extern "C" void ui_update_tray_icon(int32_t index);
// Show a native notification
extern "C" void ui_show_notification(char * message, double delay);
// Display the context menu on the tray icon.
// Payload is passed as JSON as given the complex structure, parsing
// this manually would have been complex.
extern "C" void ui_show_context_menu(char * payload);
#endif //ESPANSO_UI_H

View File

@ -0,0 +1,72 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#include "native.h"
#include "AppDelegate.h"
#import <Foundation/Foundation.h>
#include <IOKit/IOKitLib.h>
#include <stdio.h>
#include <string.h>
#include <libproc.h>
void ui_initialize(void *_self, UIOptions _options)
{
AppDelegate *delegate = [[AppDelegate alloc] init];
delegate->options = _options;
delegate->rust_instance = _self;
NSApplication * application = [NSApplication sharedApplication];
[application setDelegate:delegate];
}
int32_t ui_eventloop(EventCallback _callback)
{
AppDelegate *delegate = [[NSApplication sharedApplication] delegate];
delegate->event_callback = _callback;
[NSApp run];
return 1;
}
void ui_update_tray_icon(int32_t index)
{
dispatch_async(dispatch_get_main_queue(), ^(void) {
AppDelegate *delegate = [[NSApplication sharedApplication] delegate];
[delegate setIcon: index];
});
}
void ui_show_notification(char *message, double delay)
{
NSString *nsMessage = [NSString stringWithUTF8String:message];
dispatch_async(dispatch_get_main_queue(), ^(void) {
AppDelegate *delegate = [[NSApplication sharedApplication] delegate];
[delegate showNotification: nsMessage withDelay: delay];
});
}
void ui_show_context_menu(char *payload)
{
NSString *nsPayload = [NSString stringWithUTF8String:payload];
dispatch_async(dispatch_get_main_queue(), ^(void) {
AppDelegate *delegate = [[NSApplication sharedApplication] delegate];
[delegate popupMenu: nsPayload];
});
}

View File

@ -1,5 +1,5 @@
use espanso_detect::event::{InputEvent, Status};
use espanso_ui::{linux::LinuxUIOptions, menu::*};
use espanso_ui::{icons::TrayIcon, mac::*, menu::*, event::UIEvent::*};
use simplelog::{CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode};
fn main() {
@ -14,14 +14,24 @@ fn main() {
])
.unwrap();
// let icon_paths = vec![
// (
// espanso_ui::icons::TrayIcon::Normal,
// r"C:\Users\Freddy\AppData\Local\espanso\espanso.ico".to_string(),
// ),
// (
// espanso_ui::icons::TrayIcon::Disabled,
// r"C:\Users\Freddy\AppData\Local\espanso\espansored.ico".to_string(),
// ),
// ];
let icon_paths = vec![
(
espanso_ui::icons::TrayIcon::Normal,
r"C:\Users\Freddy\AppData\Local\espanso\espanso.ico".to_string(),
r"/Users/freddy/Library/Application Support/espanso/icon.png".to_string(),
),
(
espanso_ui::icons::TrayIcon::Disabled,
r"C:\Users\Freddy\AppData\Local\espanso\espansored.ico".to_string(),
r"/Users/freddy/Library/Application Support/espanso/icondisabled.png".to_string(),
),
];
@ -32,44 +42,55 @@ fn main() {
// notification_icon_path: r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreensmall.png"
// .to_string(),
// });
let (remote, mut eventloop) = espanso_ui::linux::create(LinuxUIOptions {
notification_icon_path: r"/home/freddy/insync/Development/Espanso/Images/icongreensmall.png".to_owned(),
let (remote, mut eventloop) = espanso_ui::mac::create(MacUIOptions {
show_icon: true,
icon_paths: &icon_paths,
});
let handle = std::thread::spawn(move || {
//let mut source = espanso_detect::win32::Win32Source::new();
let mut source = espanso_detect::evdev::EVDEVSource::new();
source.initialize();
source.eventloop(Box::new(move |event: InputEvent| {
println!("ev {:?}", event);
match event {
InputEvent::Mouse(_) => {}
InputEvent::Keyboard(evt) => {
if evt.key == espanso_detect::event::Key::Shift && evt.status == Status::Pressed {
//remote.update_tray_icon(espanso_ui::icons::TrayIcon::Disabled);
remote.show_notification("Espanso is running!");
}
}
}
}));
//let mut source = espanso_detect::x11::X11Source::new();
// source.initialize();
// source.eventloop(Box::new(move |event: InputEvent| {
// println!("ev {:?}", event);
// match event {
// InputEvent::Mouse(_) => {}
// InputEvent::Keyboard(evt) => {
// if evt.key == espanso_detect::event::Key::Shift && evt.status == Status::Pressed {
// //remote.update_tray_icon(espanso_ui::icons::TrayIcon::Disabled);
// remote.show_notification("Espanso is running!");
// }
// }
// }
// }));
});
// eventloop.initialize();
// eventloop.run(Box::new(move |event| {
// println!("ui {:?}", event);
// let menu = Menu::from(vec![
// MenuItem::Simple(SimpleMenuItem::new("open", "Open")),
// MenuItem::Separator,
// MenuItem::Sub(SubMenuItem::new(
// "Sub",
// vec![
// MenuItem::Simple(SimpleMenuItem::new("sub1", "Sub 1")),
// MenuItem::Simple(SimpleMenuItem::new("sub2", "Sub 2")),
// ],
// )),
// ])
// .unwrap();
// remote.show_context_menu(&menu);
// }))
eventloop.run();
eventloop.initialize();
eventloop.run(Box::new(move |event| {
println!("ui {:?}", event);
let menu = Menu::from(vec![
MenuItem::Simple(SimpleMenuItem::new("open", "Open")),
MenuItem::Separator,
MenuItem::Sub(SubMenuItem::new(
"Sub",
vec![
MenuItem::Simple(SimpleMenuItem::new("sub1", "Sub 1")),
MenuItem::Simple(SimpleMenuItem::new("sub2", "Sub 2")),
],
)),
])
.unwrap();
match event {
TrayIconClick => {
remote.show_context_menu(&menu);
}
ContextMenuClick(raw_id) => {
//remote.update_tray_icon(TrayIcon::Disabled);
remote.show_notification("Hello there!");
println!("item {:?}", menu.get_item_id(raw_id));
}
}
}));
}

View File

@ -38,7 +38,7 @@ fn main() {
let handle = std::thread::spawn(move || {
//let mut source = espanso_detect::win32::Win32Source::new();
let mut source = espanso_detect::x11::X11Source::new();
let mut source = espanso_detect::evdev::EVDEVSource::new();
source.initialize();
source.eventloop(Box::new(move |event: InputEvent| {
println!("ev {:?}", event);