use std::{io::Write, thread};
use ksni::TrayMethods;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
#[derive(Debug)]
struct ImeTray {
selected_im: usize,
available_ims: Vec<InputMethod>,
switching_im: bool,
notifier: UnboundedSender<TrayMessage>,
}
enum TrayMessage {
SelectIm { im: InputMethod, index: usize },
Exit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InputMethod {
English,
Chinese,
Japanese,
}
impl ToString for InputMethod {
fn to_string(&self) -> String {
match self {
InputMethod::English => "English".into(),
InputMethod::Chinese => "Chinese".into(),
InputMethod::Japanese => "Japanese".into(),
}
}
}
impl ksni::Tray for ImeTray {
fn id(&self) -> String {
env!("CARGO_PKG_NAME").into()
}
fn icon_name(&self) -> String {
"keyboard".into()
}
fn title(&self) -> String {
"Fake IME".into()
}
fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
use ksni::menu::*;
vec![
SubMenu {
label: "Input Method".into(),
submenu: vec![RadioGroup {
selected: self.selected_im,
select: Box::new(|this: &mut Self, selected| {
this.switching_im = true;
this.notifier
.send(TrayMessage::SelectIm {
im: this.available_ims[selected],
index: selected,
})
.expect("main thread alive");
}),
options: self
.available_ims
.iter()
.map(|im| RadioItem {
label: im.to_string(),
enabled: !self.switching_im,
..Default::default()
})
.collect(),
}
.into()],
..Default::default()
}
.into(),
MenuItem::Separator,
StandardItem {
label: "Reload config".into(),
icon_name: "reload".into(),
activate: Box::new(|_| {
tokio::spawn(async {
tokio::process::Command::new("echo")
.arg("Reloading config...")
.status()
.await
.expect("command executed");
});
}),
..Default::default()
}
.into(),
StandardItem {
label: "Exit".into(),
icon_name: "application-exit".into(),
activate: Box::new(|this: &mut Self| {
this.notifier
.send(TrayMessage::Exit)
.expect("main thread alive");
}),
..Default::default()
}
.into(),
]
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> std::io::Result<()> {
let (notifier, mut tray_msgs) = tokio::sync::mpsc::unbounded_channel();
let tray = ImeTray {
selected_im: 0,
available_ims: vec![InputMethod::English, InputMethod::Chinese],
switching_im: false,
notifier,
};
let handle = tray
.disable_dbus_name(ashpd::is_sandboxed())
.spawn()
.await
.unwrap();
let mut stdin = stdin_lines();
loop {
println!("0. Exit, 1. Add a Japanese Input Method");
print!("Please choose:\n> ");
std::io::stdout().flush().unwrap();
tokio::select! {
Some(line) = stdin.recv() => {
match &*line {
"0" => break Ok(()),
"1" => {
if handle
.update(|tray: &mut ImeTray| {
if tray.available_ims.contains(&InputMethod::Japanese) {
false
} else {
tray.available_ims.push(InputMethod::Japanese);
true
}
})
.await.expect("tray running") {
println!("Done")
} else {
println!("Already added")
}
}
_ => continue,
}
}
Some(msg) = tray_msgs.recv() => {
match msg {
TrayMessage::SelectIm { im, index } => {
println!("Switching Input Method to {:?}...", im);
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
handle
.update(|tray: &mut ImeTray| {
tray.switching_im = false;
tray.selected_im = index;
})
.await;
println!("Selected Input Method: {:?}", im);
}
TrayMessage::Exit => {
break Ok(());
}
}
}
}
}
}
fn stdin_lines() -> UnboundedReceiver<String> {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
thread::spawn(move || {
for line in std::io::stdin().lines() {
if tx.send(line.unwrap()).is_err() {
break;
}
}
});
rx
}