use std::collections::HashMap;
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use crossbeam_channel::Sender;
use dbus::ffidisp::stdintf::org_freedesktop_dbus::Properties;
use dbus::{
ffidisp::{MsgHandlerResult, MsgHandlerType},
Message,
};
use serde_derive::Deserialize;
use swayipc::reply::Event;
use swayipc::reply::InputChange;
use swayipc::{Connection, EventType};
use crate::blocks::{Block, ConfigBlock, Update};
use crate::config::SharedConfig;
use crate::de::deserialize_duration;
use crate::errors::*;
use crate::formatting::value::Value;
use crate::formatting::FormatTemplate;
use crate::scheduler::Task;
use crate::widgets::text::TextWidget;
use crate::widgets::I3BarWidget;
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub enum KeyboardLayoutDriver {
SetXkbMap,
LocaleBus,
KbddBus,
Sway,
}
pub trait KeyboardLayoutMonitor {
fn keyboard_layout(&self) -> Result<String>;
fn keyboard_variant(&self) -> Result<String>;
fn must_poll(&self) -> bool;
fn monitor(&self, _id: usize, _update_request: Sender<Task>) {}
}
pub struct SetXkbMap;
impl SetXkbMap {
pub fn new() -> Result<SetXkbMap> {
Ok(SetXkbMap)
}
}
fn setxkbmap_layouts() -> Result<String> {
let output = Command::new("setxkbmap")
.args(&["-query"])
.output()
.block_error("keyboard_layout", "Failed to execute setxkbmap.")
.and_then(|raw| {
String::from_utf8(raw.stdout).block_error("keyboard_layout", "Non-UTF8 input.")
})?;
let layout = output
.split('\n')
.find(|line| line.starts_with("layout"))
.block_error(
"keyboard_layout",
"Could not find the layout entry from setxkbmap.",
)?
.split(char::is_whitespace)
.last();
layout.map(|s| s.to_string()).block_error(
"keyboard_layout",
"Could not read the layout entry from setxkbmap.",
)
}
impl KeyboardLayoutMonitor for SetXkbMap {
fn keyboard_layout(&self) -> Result<String> {
setxkbmap_layouts()
}
fn keyboard_variant(&self) -> Result<String> {
Ok("N/A".to_string())
}
fn must_poll(&self) -> bool {
true
}
}
pub struct LocaleBus {
con: dbus::ffidisp::Connection,
}
impl LocaleBus {
pub fn new() -> Result<Self> {
let con = dbus::ffidisp::Connection::get_private(dbus::ffidisp::BusType::System)
.block_error("locale", "Failed to establish D-Bus connection.")?;
Ok(LocaleBus { con })
}
}
impl KeyboardLayoutMonitor for LocaleBus {
fn keyboard_layout(&self) -> Result<String> {
self.con
.with_path("org.freedesktop.locale1", "/org/freedesktop/locale1", 1000)
.get("org.freedesktop.locale1", "X11Layout")
.block_error("locale", "Failed to get X11Layout property.")
}
fn keyboard_variant(&self) -> Result<String> {
self.con
.with_path("org.freedesktop.locale1", "/org/freedesktop/locale1", 1000)
.get("org.freedesktop.locale1", "X11Variant")
.block_error("locale", "Failed to get X11Variant property.")
}
fn must_poll(&self) -> bool {
false
}
fn monitor(&self, id: usize, update_request: Sender<Task>) {
thread::Builder::new()
.name("keyboard_layout".into())
.spawn(move || {
let con = dbus::ffidisp::Connection::get_private(dbus::ffidisp::BusType::System)
.expect("Failed to establish D-Bus connection.");
let rule = "type='signal',\
path='/org/freedesktop/locale1',\
interface='org.freedesktop.DBus.Properties',\
member='PropertiesChanged'";
con.incoming(10_000).next();
con.add_match(&rule)
.expect("Failed to add D-Bus match rule.");
loop {
if con.incoming(10_000).next().is_some() {
update_request
.send(Task {
id,
update_time: Instant::now(),
})
.unwrap();
}
}
})
.unwrap();
}
}
pub struct KbdDaemonBus {
kbdd_layout_id: Arc<Mutex<u32>>,
}
impl KbdDaemonBus {
pub fn new() -> Result<Self> {
Command::new("setxkbmap")
.arg("-version")
.output()
.block_error("kbddaemonbus", "setxkbmap not found")?;
let layout_id = KbdDaemonBus::get_initial_layout_id()?;
Ok(KbdDaemonBus {
kbdd_layout_id: Arc::new(Mutex::new(layout_id)),
})
}
fn get_initial_layout_id() -> Result<u32> {
let c = dbus::ffidisp::Connection::get_private(dbus::ffidisp::BusType::Session)
.block_error("kbddaemonbus", "can't connect to dbus")?;
let send_msg = Message::new_method_call(
"ru.gentoo.KbddService",
"/ru/gentoo/KbddService",
"ru.gentoo.kbdd",
"getCurrentLayout",
)
.block_error("kbddaemonbus", "Create get-layout-id message failure")?;
let repl_msg = c
.send_with_reply_and_block(send_msg, 5000)
.block_error("kbddaemonbus", "Is kbdd running?")?;
let current_layout_id: u32 = repl_msg
.get1()
.ok_or("")
.block_error("kbddaemonbus", "dbus kbdd response error")?;
Ok(current_layout_id)
}
}
impl KeyboardLayoutMonitor for KbdDaemonBus {
fn keyboard_layout(&self) -> Result<String> {
let layouts_str = setxkbmap_layouts()?;
let idx = *self.kbdd_layout_id.lock().unwrap();
let split = layouts_str.split(',').nth(idx as usize);
match split {
Some(s) => Ok(s.split(':').next().unwrap().to_string()),
None => Ok(layouts_str),
}
}
fn keyboard_variant(&self) -> Result<String> {
Ok("N/A".to_string())
}
fn must_poll(&self) -> bool {
false
}
fn monitor(&self, id: usize, update_request: Sender<Task>) {
let arc = Arc::clone(&self.kbdd_layout_id);
thread::Builder::new()
.name("keyboard_layout".into())
.spawn(move || {
let c = dbus::ffidisp::Connection::get_private(dbus::ffidisp::BusType::Session)
.unwrap();
c.add_match(
"interface='ru.gentoo.kbdd',\
member='layoutChanged',\
path='/ru/gentoo/KbddService'",
)
.expect("Failed to add D-Bus match rule, is kbdd started?");
c.incoming(10_000).next();
c.add_handler(KbddMessageHandler(arc));
loop {
for ci in c.iter(100_000) {
if let dbus::ffidisp::ConnectionItem::Signal(_) = ci {
update_request
.send(Task {
id,
update_time: Instant::now(),
})
.unwrap();
}
}
}
})
.unwrap();
}
}
struct KbddMessageHandler(Arc<Mutex<u32>>);
impl dbus::ffidisp::MsgHandler for KbddMessageHandler {
fn handler_type(&self) -> MsgHandlerType {
dbus::ffidisp::MsgHandlerType::MsgType(dbus::MessageType::Signal)
}
fn handle_msg(&mut self, msg: &Message) -> Option<MsgHandlerResult> {
let layout: Option<u32> = msg.get1();
if let Some(idx) = layout {
let mut val = self.0.lock().unwrap();
*val = idx;
}
Some(MsgHandlerResult {
handled: false,
done: false,
reply: vec![],
})
}
}
pub struct Sway {
sway_kb_layout: Arc<Mutex<String>>,
}
impl Sway {
pub fn new(sway_kb_identifier: Option<String>) -> Result<Self> {
let layout = swayipc::Connection::new()
.unwrap()
.get_inputs()
.unwrap()
.into_iter()
.find(|input| {
sway_kb_identifier
.as_ref()
.map(|s| s == &input.identifier)
.unwrap_or(true)
&& input.input_type == "keyboard"
})
.and_then(|input| input.xkb_active_layout_name)
.block_error("sway", "Failed to get xkb_active_layout_name.")?;
Ok(Sway {
sway_kb_layout: Arc::new(Mutex::new(layout)),
})
}
}
impl KeyboardLayoutMonitor for Sway {
fn keyboard_layout(&self) -> Result<String> {
let layout = self.sway_kb_layout.lock().unwrap();
let (layout, _variant) = match layout.find('(') {
Some(i) => layout.split_at(i),
None => return Ok(layout.to_string()),
};
Ok(layout
.split_whitespace()
.next()
.unwrap_or(layout)
.to_string())
}
fn keyboard_variant(&self) -> Result<String> {
let layout = self.sway_kb_layout.lock().unwrap();
let (_layout, variant) = match layout.find('(') {
Some(i) => layout.split_at(i),
None => return Ok("N/A".to_string()),
};
Ok(variant[1..variant.len() - 1].to_string())
}
fn must_poll(&self) -> bool {
false
}
fn monitor(&self, id: usize, update_request: Sender<Task>) {
let arc = Arc::clone(&self.sway_kb_layout);
thread::Builder::new()
.name("keyboard_layout".into())
.spawn(move || {
for event in Connection::new()
.unwrap()
.subscribe(&[EventType::Input])
.unwrap()
{
match event.unwrap() {
Event::Input(e) => match e.change {
InputChange::XkbLayout => {
if let Some(name) = e.input.xkb_active_layout_name {
let mut layout = arc.lock().unwrap();
*layout = name;
}
update_request
.send(Task {
id,
update_time: Instant::now(),
})
.unwrap();
}
InputChange::XkbKeymap => {
if let Some(name) = e.input.xkb_active_layout_name {
let mut layout = arc.lock().unwrap();
*layout = name;
}
update_request
.send(Task {
id,
update_time: Instant::now(),
})
.unwrap();
}
_ => {}
},
_ => unreachable!(),
}
}
})
.unwrap();
}
}
#[derive(Deserialize, Debug, Clone)]
#[serde(default, deny_unknown_fields)]
pub struct KeyboardLayoutConfig {
pub format: String,
driver: KeyboardLayoutDriver,
#[serde(deserialize_with = "deserialize_duration")]
interval: Duration,
sway_kb_identifier: Option<String>,
mappings: Option<HashMap<String, String>>,
}
impl Default for KeyboardLayoutConfig {
fn default() -> Self {
Self {
format: "{layout}".to_string(),
driver: KeyboardLayoutDriver::SetXkbMap,
interval: Duration::from_secs(60),
sway_kb_identifier: None,
mappings: None,
}
}
}
pub struct KeyboardLayout {
id: usize,
output: TextWidget,
monitor: Box<dyn KeyboardLayoutMonitor>,
update_interval: Option<Duration>,
format: FormatTemplate,
mappings: Option<HashMap<String, String>>,
}
impl ConfigBlock for KeyboardLayout {
type Config = KeyboardLayoutConfig;
fn new(
id: usize,
block_config: Self::Config,
shared_config: SharedConfig,
send: Sender<Task>,
) -> Result<Self> {
let monitor: Box<dyn KeyboardLayoutMonitor> = match block_config.driver {
KeyboardLayoutDriver::SetXkbMap => Box::new(SetXkbMap::new()?),
KeyboardLayoutDriver::LocaleBus => {
let monitor = LocaleBus::new()?;
monitor.monitor(id, send);
Box::new(monitor)
}
KeyboardLayoutDriver::KbddBus => {
let monitor = KbdDaemonBus::new()?;
monitor.monitor(id, send);
Box::new(monitor)
}
KeyboardLayoutDriver::Sway => {
let monitor = Sway::new(block_config.sway_kb_identifier)?;
monitor.monitor(id, send);
Box::new(monitor)
}
};
let update_interval = if monitor.must_poll() {
Some(block_config.interval)
} else {
None
};
let output = TextWidget::new(id, 0, shared_config);
Ok(KeyboardLayout {
id,
output,
monitor,
update_interval,
format: FormatTemplate::from_string(&block_config.format).block_error(
"keyboard_layout",
"Invalid format specified for keyboard_layout",
)?,
mappings: block_config.mappings,
})
}
}
impl Block for KeyboardLayout {
fn id(&self) -> usize {
self.id
}
fn update(&mut self) -> Result<Option<Update>> {
let mut layout = self.monitor.keyboard_layout()?;
let variant = self.monitor.keyboard_variant()?;
if let Some(ref mappings) = self.mappings {
if let Some(mapped) = mappings.get(&format!("{} ({})", layout, variant)) {
layout = mapped.to_string();
}
}
let values = map!(
"layout" => Value::from_string(layout),
"variant" => Value::from_string(variant)
);
self.output.set_text(self.format.render(&values)?);
Ok(self.update_interval.map(|d| d.into()))
}
fn view(&self) -> Vec<&dyn I3BarWidget> {
vec![&self.output]
}
}