vietc/cli/src/main.rs

325 lines
11 KiB
Rust

use std::io::{self, Write};
use vietc_engine::{Engine, EngineEvent, EventStore, InputEvent, InputMethod};
struct CliState {
engine: Engine,
method: InputMethod,
events: EventStore,
macros: Vec<(String, String)>,
auto_restore: bool,
}
impl CliState {
fn new() -> Self {
Self {
engine: Engine::new(InputMethod::Telex),
method: InputMethod::Telex,
events: EventStore::new(),
macros: Vec::new(),
auto_restore: true,
}
}
fn set_method(&mut self, method: InputMethod) {
self.method = method;
self.engine.set_method(method);
}
fn status(&self) {
println!(" Method: {:?}", self.method);
println!(" Enabled: {}", self.engine.is_enabled());
println!(" Auto-restore: {}", self.auto_restore);
println!(" Buffer: {:?}", self.engine.buffer());
println!(" Macros: {} defined", self.macros.len());
for (s, e) in &self.macros {
println!(" {} -> {}", s, e);
}
println!(" Events: {} recorded", self.events.len());
}
}
fn main() {
let mut state = CliState::new();
print_help();
loop {
print!("> ");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let input = input.trim();
if input.is_empty() {
continue;
}
if input == "quit" || input == "exit" {
break;
}
if input.starts_with(':') {
handle_command(&mut state, input);
continue;
}
state.engine.reset();
let mut output = String::new();
let mut events = Vec::new();
for ch in input.chars() {
state.events.push(InputEvent::KeyTyped(ch));
match state.engine.process_key(ch) {
None => {
output.push(ch);
}
Some(event) => {
events.push((ch, event.clone()));
match &event {
EngineEvent::Insert(text) | EngineEvent::Flush(text) => {
output.push_str(text);
}
EngineEvent::Paste(text) => {
output.push_str(text);
}
EngineEvent::Replace { backspaces, insert } => {
for _ in 0..*backspaces {
output.push('\x08');
}
output.push_str(insert);
if is_flush_char(ch) {
output.push(ch);
}
}
EngineEvent::UndoTones { backspaces, restored } => {
for _ in 0..*backspaces {
output.push('\x08');
}
output.push_str(restored);
}
EngineEvent::AutoRestore(word) => {
for _ in 0..word.len() {
output.push('\x08');
}
output.push_str(word);
}
}
}
}
}
if let Some(event) = state.engine.flush() {
match &event {
EngineEvent::Flush(text) | EngineEvent::Insert(text) => {
output.push_str(text);
}
_ => {}
}
events.push(('\n', event));
}
println!(" Events: {:?}", events);
println!(" Raw: {:?}", output);
let display = apply_backspaces(&output);
println!(" Screen: {}", display);
}
}
fn print_help() {
println!("Viet+ IME - Test Harness");
println!("=========================");
println!("Type text with VNI/Telex to see engine output.");
println!();
println!("Commands:");
println!(" :help Show this help");
println!(" :status Show engine state");
println!(" :vi Enable Vietnamese mode");
println!(" :en Disable Vietnamese mode");
println!(" :ar on|off Toggle auto-restore");
println!(" :vni Switch to VNI input");
println!(" :telex Switch to Telex input");
println!(" :reset Reset engine buffer");
println!(" :buffer Show composing buffer");
println!(" :events Show event store history");
println!(" :events clear Clear event store");
println!(" :macros List macros");
println!(" :macro add <s> <e> Add macro shortcut->expansion");
println!(" :macro rm <s> Remove a macro");
println!(" :macro clear Clear all macros");
println!(" quit/exit Quit");
println!();
}
fn handle_command(state: &mut CliState, input: &str) {
let parts: Vec<&str> = input.splitn(4, ' ').collect();
let cmd = parts[0];
match cmd {
":help" | ":h" => print_help(),
":status" | ":st" => state.status(),
":vi" => {
state.engine.set_enabled(true);
println!("[Vietnamese mode ON]");
}
":en" => {
state.engine.set_enabled(false);
println!("[Vietnamese mode OFF]");
}
":ar" => {
if parts.len() < 2 {
println!("[Usage: :ar on|off]");
return;
}
match parts[1] {
"on" => {
state.auto_restore = true;
state.engine.set_auto_restore(true);
println!("[Auto-restore ON]");
}
"off" => {
state.auto_restore = false;
state.engine.set_auto_restore(false);
println!("[Auto-restore OFF]");
}
_ => println!("[Usage: :ar on|off]"),
}
}
":vni" => {
state.set_method(InputMethod::Vni);
println!("[Switched to VNI]");
}
":telex" => {
state.set_method(InputMethod::Telex);
println!("[Switched to Telex]");
}
":reset" => {
state.engine.reset();
println!("[Engine reset]");
}
":buffer" => {
println!("[Buffer: {:?}]", state.engine.buffer());
}
":events" | ":ev" => {
if parts.len() > 1 && parts[1] == "clear" {
state.events.clear();
println!("[Event store cleared]");
return;
}
if state.events.is_empty() {
println!("[No events]");
} else {
println!("[Events: {}]", state.events.len());
for (i, event) in state.events.iter().enumerate() {
println!(" {}: {:?}", i, event);
}
println!(" Raw keystrokes: {:?}", state.events.raw_keystrokes());
println!(" Pattern hash: {}", state.events.pattern_hash());
}
}
":macros" => {
if state.macros.is_empty() {
println!("[No macros defined]");
} else {
println!("[Macros: {}]", state.macros.len());
for (s, e) in &state.macros {
println!(" {} -> {}", s, e);
}
}
}
":macro" => {
if parts.len() < 2 {
println!("[Usage: :macro add <shortcut> <expansion> or :macro rm <s> or :macro clear]");
return;
}
match parts[1] {
"add" | "a" => {
if parts.len() < 4 {
println!("[Usage: :macro add <shortcut> <expansion>]");
return;
}
let shortcut = parts[2].to_string();
let expansion = parts[3].to_string();
state.engine.add_macro(shortcut.clone(), expansion.clone());
if let Some(pos) = state.macros.iter().position(|(s, _)| *s == shortcut) {
state.macros[pos].1 = expansion.clone();
} else {
state.macros.push((shortcut.clone(), expansion.clone()));
}
println!("[Macro added: {} -> {}]", shortcut, expansion);
}
"rm" | "remove" | "del" => {
if parts.len() < 3 {
println!("[Usage: :macro rm <shortcut>]");
return;
}
let shortcut = parts[2];
if let Some(pos) = state.macros.iter().position(|(s, _)| s == shortcut) {
state.macros.remove(pos);
state.engine.clear_macros();
for (s, e) in &state.macros {
state.engine.add_macro(s.clone(), e.clone());
}
println!("[Macro removed: {}]", shortcut);
} else {
println!("[Macro not found: {}]", shortcut);
}
}
"clear" | "c" => {
state.engine.clear_macros();
state.macros.clear();
println!("[All macros cleared]");
}
_ => {
let shortcut = parts[1].to_string();
let expansion = parts.get(2).map(|s| s.to_string()).unwrap_or_default();
if expansion.is_empty() {
println!("[Usage: :macro add <shortcut> <expansion>]");
return;
}
state.engine.add_macro(shortcut.clone(), expansion.clone());
if let Some(pos) = state.macros.iter().position(|(s, _)| *s == shortcut) {
state.macros[pos].1 = expansion.clone();
} else {
state.macros.push((shortcut.clone(), expansion.clone()));
}
println!("[Macro added: {} -> {}]", shortcut, expansion);
}
}
}
_ => {
println!("[Unknown command: {}. Type :help for available commands]", cmd);
}
}
}
fn is_flush_char(ch: char) -> bool {
matches!(ch, ' ' | '\t' | '.' | ',' | '!' | '?' | ';' | ':' | '\n')
}
fn apply_backspaces(s: &str) -> String {
let mut result = String::new();
for ch in s.chars() {
if ch == '\x08' {
result.pop();
} else {
result.push(ch);
}
}
result
}