From 3aba5b5fdcf1ad6d9266373d22959b6a2b921faa Mon Sep 17 00:00:00 2001 From: David Onuh Date: Thu, 3 Oct 2024 01:29:25 +0100 Subject: [PATCH 1/5] unfucking the cli: used raw mode to extrapolate events properly --- ahnlich/cli/src/term.rs | 153 ++++++++++++++++++++++++++++++---------- 1 file changed, 117 insertions(+), 36 deletions(-) diff --git a/ahnlich/cli/src/term.rs b/ahnlich/cli/src/term.rs index b428b6bc..2889d40d 100644 --- a/ahnlich/cli/src/term.rs +++ b/ahnlich/cli/src/term.rs @@ -1,14 +1,39 @@ +use crossterm::event::{ + poll, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, +}; + use crossterm::{ + cursor, event::{self, Event, KeyCode, KeyEvent}, + execute, queue, style::{Color, Print, SetForegroundColor, Stylize}, + terminal::{disable_raw_mode, enable_raw_mode}, ExecutableCommand, }; -use std::io::{self, stdout, Write}; +use std::io::{self, stdout, Stdout, Write}; use crate::connect::AgentPool; const RESERVED_WORDS: [&str; 3] = ["ping", "infoserver", "createpredindex"]; +#[derive(Debug)] +enum SpecialEntry { + Enter, + Up, + Down, + Break, + Left, + Right, +} + +#[derive(Debug)] +enum Entry { + Char(char), + Special(SpecialEntry), + Other(KeyCode), + None, +} + pub struct Term { client_pool: AgentPool, } @@ -18,23 +43,20 @@ impl Term { Self { client_pool } } - pub(crate) fn read_line(&self) -> io::Result { - let mut line = String::new(); - while let Event::Key(KeyEvent { code, .. }) = event::read()? { - match code { - KeyCode::Enter => { - break; - } - KeyCode::Char(c) => { - line.push(c); - } - KeyCode::Esc => { - break; - } - _ => {} - } + pub(crate) fn read_char(&self) -> io::Result { + if let Event::Key(KeyEvent { code, .. }) = event::read()? { + Ok(match code { + KeyCode::Enter => Entry::Special(SpecialEntry::Enter), + KeyCode::Char(c) => Entry::Char(c), + KeyCode::Left => Entry::Special(SpecialEntry::Left), + KeyCode::Up => Entry::Special(SpecialEntry::Up), + KeyCode::Down => Entry::Special(SpecialEntry::Down), + KeyCode::Right => Entry::Special(SpecialEntry::Right), + _ => Entry::Other(code), + }) + } else { + Ok(Entry::None) } - Ok(line) } pub fn welcome_message(&self) -> io::Result<()> { let mut stdout = stdout(); @@ -48,50 +70,109 @@ impl Term { Ok(()) } - pub(crate) fn ahnlich_prompt(&self) -> io::Result<()> { - let mut stdout = stdout(); + pub(crate) fn ahnlich_prompt(&self, stdout: &mut Stdout) -> io::Result<()> { stdout.execute(SetForegroundColor(Color::White))?; stdout.execute(Print(">>> "))?; stdout.execute(SetForegroundColor(Color::White))?; + stdout.flush()?; - //stdout.flush()?; Ok(()) } - pub(crate) fn print_query(&self, query: &str) -> io::Result<()> { - self.ahnlich_prompt()?; - let output = String::from_iter(query.split(' ').map(|ex| { - if RESERVED_WORDS.contains(&(ex.to_lowercase().as_str())) { - format!("{} ", ex.magenta()) - } else { - format!("{} ", ex.white()) - } - })); + // pub(crate) fn print_query(&self, query: &str) -> io::Result<()> { + // self.ahnlich_prompt()?; + // let output = String::from_iter(query.split(' ').map(|ex| { + // if RESERVED_WORDS.contains(&(ex.to_lowercase().as_str())) { + // format!("{} ", ex.magenta()) + // } else { + // format!("{} ", ex.white()) + // } + // })); - println!("{output}"); + // println!("{output}"); - Ok(()) + // Ok(()) + // } + + fn read_line(&self, stdout: &mut Stdout) -> io::Result { + let (start_pos_col, _) = cursor::position()?; + let mut output = String::new(); + + loop { + let char = self.read_char()?; + let (current_pos_col, _) = cursor::position()?; + match char { + Entry::Char(c) => { + output.push(c); + stdout.execute(Print(c))?; + stdout.flush()?; + } + Entry::Special(special) => match special { + SpecialEntry::Up | SpecialEntry::Down => { + continue; + } + SpecialEntry::Enter | SpecialEntry::Break => { + queue!(stdout, Print("\n"), cursor::MoveToColumn(0))?; + stdout.flush()?; + break; + } + SpecialEntry::Left => { + if start_pos_col < current_pos_col { + stdout.execute(cursor::MoveLeft(1))?; + } + } + SpecialEntry::Right => { + if start_pos_col + output.len() as u16 > current_pos_col { + stdout.execute(cursor::MoveRight(1))?; + } + } + }, + Entry::Other(_) | Entry::None => { + continue; + } + } + } + Ok(output) } pub async fn run(&self) -> io::Result<()> { + enable_raw_mode()?; + let mut stdout = stdout(); + stdout.execute(cursor::EnableBlinking)?; + stdout.execute(cursor::SetCursorStyle::BlinkingBar)?; + loop { - self.ahnlich_prompt()?; - let input = self.read_line()?; + self.ahnlich_prompt(&mut stdout)?; + let input = self.read_line(&mut stdout)?; match input.as_str() { "quit" | "exit()" => break, command => { - self.print_query(command)?; let response = self.client_pool.parse_queries(command).await; match response { Ok(success) => { - println!("{}", success.join("\n\n")) + for msg in success { + queue!( + stdout, + Print(format!("{}\n", msg)), + cursor::MoveToColumn(0) + )?; + } + stdout.flush()?; + } + Err(err) => { + queue!( + stdout, + Print(format!("{}\n", err.red())), + cursor::MoveToColumn(0) + )?; + stdout.flush()?; } - Err(err) => println!("{}", err.red()), } } }; } + disable_raw_mode()?; Ok(()) } } From 40ddc75a417e5aff098e731fb2e480d086d3738a Mon Sep 17 00:00:00 2001 From: David Onuh Date: Thu, 3 Oct 2024 03:10:26 +0100 Subject: [PATCH 2/5] add logic for deleting entries --- ahnlich/cli/src/term.rs | 99 ++++++++++++++++++++++++++++++++--------- 1 file changed, 78 insertions(+), 21 deletions(-) diff --git a/ahnlich/cli/src/term.rs b/ahnlich/cli/src/term.rs index 2889d40d..38cea992 100644 --- a/ahnlich/cli/src/term.rs +++ b/ahnlich/cli/src/term.rs @@ -1,16 +1,13 @@ -use crossterm::event::{ - poll, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, -}; - use crossterm::{ cursor, event::{self, Event, KeyCode, KeyEvent}, - execute, queue, + queue, style::{Color, Print, SetForegroundColor, Stylize}, terminal::{disable_raw_mode, enable_raw_mode}, ExecutableCommand, }; use std::io::{self, stdout, Stdout, Write}; +use std::usize; use crate::connect::AgentPool; @@ -24,6 +21,7 @@ enum SpecialEntry { Break, Left, Right, + Del, } #[derive(Debug)] @@ -52,6 +50,7 @@ impl Term { KeyCode::Up => Entry::Special(SpecialEntry::Up), KeyCode::Down => Entry::Special(SpecialEntry::Down), KeyCode::Right => Entry::Special(SpecialEntry::Right), + KeyCode::Backspace => Entry::Special(SpecialEntry::Del), _ => Entry::Other(code), }) } else { @@ -72,27 +71,72 @@ impl Term { pub(crate) fn ahnlich_prompt(&self, stdout: &mut Stdout) -> io::Result<()> { stdout.execute(SetForegroundColor(Color::White))?; - stdout.execute(Print(">>> "))?; + stdout.execute(Print(">>>"))?; stdout.execute(SetForegroundColor(Color::White))?; stdout.flush()?; Ok(()) } - // pub(crate) fn print_query(&self, query: &str) -> io::Result<()> { - // self.ahnlich_prompt()?; - // let output = String::from_iter(query.split(' ').map(|ex| { - // if RESERVED_WORDS.contains(&(ex.to_lowercase().as_str())) { - // format!("{} ", ex.magenta()) - // } else { - // format!("{} ", ex.white()) - // } - // })); + pub(crate) fn format_output(&self, query: &str) -> String { + let output = String::from_iter(query.split(' ').map(|ex| { + if RESERVED_WORDS.contains(&(ex.to_lowercase().as_str())) { + format!("{}", ex.magenta()) + } else { + format!("{}", ex.white()) + } + })); + output + } + + fn remove_at_pos(&self, input: &mut String, char_index: u16) { + let byte_index = input + .char_indices() + .nth(char_index as usize) + .map(|(entry, _)| entry) + .expect(&format!( + "Index out of bounds {} --> {}", + input.len(), + char_index + )); + + input.remove(byte_index); + } - // println!("{output}"); + fn move_to_pos_and_print( + &self, + stdout: &mut Stdout, + output: &str, + col_pos: u16, + ) -> io::Result<()> { + let formatted_output = self.format_output(&output); + queue!( + stdout, + cursor::MoveToColumn(col_pos), + Print(formatted_output) + )?; + stdout.flush()?; + Ok(()) + } - // Ok(()) - // } + fn delete_and_print_less( + &self, + stdout: &mut Stdout, + output: &str, + col_pos: u16, + ) -> io::Result<()> { + let formatted_output = self.format_output(&output); + let clean = vec![" "; output.len() + 1]; + queue!( + stdout, + cursor::MoveToColumn(col_pos), + Print(format!("{}", clean.join(""))), + cursor::MoveToColumn(col_pos), + Print(formatted_output) + )?; + stdout.flush()?; + Ok(()) + } fn read_line(&self, stdout: &mut Stdout) -> io::Result { let (start_pos_col, _) = cursor::position()?; @@ -104,8 +148,7 @@ impl Term { match char { Entry::Char(c) => { output.push(c); - stdout.execute(Print(c))?; - stdout.flush()?; + self.move_to_pos_and_print(stdout, &output, start_pos_col)?; } Entry::Special(special) => match special { SpecialEntry::Up | SpecialEntry::Down => { @@ -126,6 +169,18 @@ impl Term { stdout.execute(cursor::MoveRight(1))?; } } + SpecialEntry::Del => { + if current_pos_col == start_pos_col { + continue; + } + let output_current_pos = current_pos_col - start_pos_col; + + if !output.is_empty() { + self.remove_at_pos(&mut output, output_current_pos - 1); + self.delete_and_print_less(stdout, &output, start_pos_col)?; + stdout.execute(cursor::MoveToColumn(current_pos_col - 1))?; + } + } }, Entry::Other(_) | Entry::None => { continue; @@ -145,12 +200,13 @@ impl Term { self.ahnlich_prompt(&mut stdout)?; let input = self.read_line(&mut stdout)?; match input.as_str() { - "quit" | "exit()" => break, + "quit" | "exit" | "exit()" => break, command => { let response = self.client_pool.parse_queries(command).await; match response { Ok(success) => { + disable_raw_mode()?; for msg in success { queue!( stdout, @@ -159,6 +215,7 @@ impl Term { )?; } stdout.flush()?; + enable_raw_mode()? } Err(err) => { queue!( From 65840a13092f55fffe704528449db2b9b2ab3c48 Mon Sep 17 00:00:00 2001 From: David Onuh Date: Thu, 3 Oct 2024 03:17:52 +0100 Subject: [PATCH 3/5] clippy fixes --- ahnlich/cli/src/term.rs | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/ahnlich/cli/src/term.rs b/ahnlich/cli/src/term.rs index 38cea992..3cdfc62f 100644 --- a/ahnlich/cli/src/term.rs +++ b/ahnlich/cli/src/term.rs @@ -7,7 +7,6 @@ use crossterm::{ ExecutableCommand, }; use std::io::{self, stdout, Stdout, Write}; -use std::usize; use crate::connect::AgentPool; @@ -18,7 +17,6 @@ enum SpecialEntry { Enter, Up, Down, - Break, Left, Right, Del, @@ -28,7 +26,6 @@ enum SpecialEntry { enum Entry { Char(char), Special(SpecialEntry), - Other(KeyCode), None, } @@ -41,7 +38,7 @@ impl Term { Self { client_pool } } - pub(crate) fn read_char(&self) -> io::Result { + fn read_char(&self) -> io::Result { if let Event::Key(KeyEvent { code, .. }) = event::read()? { Ok(match code { KeyCode::Enter => Entry::Special(SpecialEntry::Enter), @@ -51,7 +48,7 @@ impl Term { KeyCode::Down => Entry::Special(SpecialEntry::Down), KeyCode::Right => Entry::Special(SpecialEntry::Right), KeyCode::Backspace => Entry::Special(SpecialEntry::Del), - _ => Entry::Other(code), + _ => Entry::None, }) } else { Ok(Entry::None) @@ -94,11 +91,7 @@ impl Term { .char_indices() .nth(char_index as usize) .map(|(entry, _)| entry) - .expect(&format!( - "Index out of bounds {} --> {}", - input.len(), - char_index - )); + .unwrap_or_else(|| panic!("Index out of bounds {} --> {}", input.len(), char_index)); input.remove(byte_index); } @@ -109,7 +102,7 @@ impl Term { output: &str, col_pos: u16, ) -> io::Result<()> { - let formatted_output = self.format_output(&output); + let formatted_output = self.format_output(output); queue!( stdout, cursor::MoveToColumn(col_pos), @@ -125,12 +118,12 @@ impl Term { output: &str, col_pos: u16, ) -> io::Result<()> { - let formatted_output = self.format_output(&output); + let formatted_output = self.format_output(output); let clean = vec![" "; output.len() + 1]; queue!( stdout, cursor::MoveToColumn(col_pos), - Print(format!("{}", clean.join(""))), + Print(clean.join("").to_string()), cursor::MoveToColumn(col_pos), Print(formatted_output) )?; @@ -154,7 +147,7 @@ impl Term { SpecialEntry::Up | SpecialEntry::Down => { continue; } - SpecialEntry::Enter | SpecialEntry::Break => { + SpecialEntry::Enter => { queue!(stdout, Print("\n"), cursor::MoveToColumn(0))?; stdout.flush()?; break; @@ -182,7 +175,7 @@ impl Term { } } }, - Entry::Other(_) | Entry::None => { + Entry::None => { continue; } } From 3b71a199ec955b394c69098b8d6f48684f717ad5 Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Fri, 4 Oct 2024 01:20:27 +0200 Subject: [PATCH 4/5] Implemented ctrl-C exit, clearsreen (ctrl-l) and fix spaces not printing --- ahnlich/cli/Cargo.toml | 2 +- ahnlich/cli/src/connect.rs | 8 ++ ahnlich/cli/src/term.rs | 149 +++++++++++++++++++---------- ahnlich/dsl/src/ai.rs | 34 +++---- ahnlich/dsl/src/db.rs | 34 +++---- ahnlich/dsl/src/syntax/syntax.pest | 12 +-- 6 files changed, 148 insertions(+), 91 deletions(-) diff --git a/ahnlich/cli/Cargo.toml b/ahnlich/cli/Cargo.toml index 9bed2c8c..4690c2c2 100644 --- a/ahnlich/cli/Cargo.toml +++ b/ahnlich/cli/Cargo.toml @@ -13,7 +13,7 @@ path = "src/lib.rs" [dependencies] -crossterm = "0.28.1" +crossterm = { version = "0.28.1", feature = ["bracketed-paste"]} clap.workspace = true dsl = { path = "../dsl", version = "*" } thiserror.workspace = true diff --git a/ahnlich/cli/src/connect.rs b/ahnlich/cli/src/connect.rs index 5dd2e78d..82d1679a 100644 --- a/ahnlich/cli/src/connect.rs +++ b/ahnlich/cli/src/connect.rs @@ -36,6 +36,14 @@ impl AgentPool { } } + /// Returns the commands for the agent pool in question + pub fn commands(&self) -> &[&str] { + match self { + AgentPool::AI(_) => dsl::ai::COMMANDS, + AgentPool::DB(_) => dsl::db::COMMANDS, + } + } + /// Checks if the connection to to a host and post is alive, also checks the cli is connected /// to the right server( ahnlich ai or db) pub async fn is_valid_connection(&self) -> Result { diff --git a/ahnlich/cli/src/term.rs b/ahnlich/cli/src/term.rs index 3cdfc62f..68ae19b6 100644 --- a/ahnlich/cli/src/term.rs +++ b/ahnlich/cli/src/term.rs @@ -3,15 +3,13 @@ use crossterm::{ event::{self, Event, KeyCode, KeyEvent}, queue, style::{Color, Print, SetForegroundColor, Stylize}, - terminal::{disable_raw_mode, enable_raw_mode}, + terminal::{self, disable_raw_mode, enable_raw_mode}, ExecutableCommand, }; use std::io::{self, stdout, Stdout, Write}; use crate::connect::AgentPool; -const RESERVED_WORDS: [&str; 3] = ["ping", "infoserver", "createpredindex"]; - #[derive(Debug)] enum SpecialEntry { Enter, @@ -20,6 +18,8 @@ enum SpecialEntry { Left, Right, Del, + Exit, + ClrScr, } #[derive(Debug)] @@ -29,6 +29,12 @@ enum Entry { None, } +#[derive(Debug)] +enum LineResult { + Command(String), + Exit, +} + pub struct Term { client_pool: AgentPool, } @@ -39,29 +45,40 @@ impl Term { } fn read_char(&self) -> io::Result { - if let Event::Key(KeyEvent { code, .. }) = event::read()? { - Ok(match code { - KeyCode::Enter => Entry::Special(SpecialEntry::Enter), - KeyCode::Char(c) => Entry::Char(c), - KeyCode::Left => Entry::Special(SpecialEntry::Left), - KeyCode::Up => Entry::Special(SpecialEntry::Up), - KeyCode::Down => Entry::Special(SpecialEntry::Down), - KeyCode::Right => Entry::Special(SpecialEntry::Right), - KeyCode::Backspace => Entry::Special(SpecialEntry::Del), - _ => Entry::None, - }) - } else { - Ok(Entry::None) + match event::read()? { + Event::Key(KeyEvent { + code, modifiers, .. + }) => { + if code == KeyCode::Char('c') && modifiers == event::KeyModifiers::CONTROL { + return Ok(Entry::Special(SpecialEntry::Exit)); + } + if code == KeyCode::Char('l') && modifiers == event::KeyModifiers::CONTROL { + return Ok(Entry::Special(SpecialEntry::ClrScr)); + } + Ok(match code { + KeyCode::Enter => Entry::Special(SpecialEntry::Enter), + KeyCode::Char(c) => Entry::Char(c), + KeyCode::Left => Entry::Special(SpecialEntry::Left), + KeyCode::Up => Entry::Special(SpecialEntry::Up), + KeyCode::Down => Entry::Special(SpecialEntry::Down), + KeyCode::Right => Entry::Special(SpecialEntry::Right), + KeyCode::Backspace => Entry::Special(SpecialEntry::Del), + _ => Entry::None, + }) + } + _ => Ok(Entry::None), } } pub fn welcome_message(&self) -> io::Result<()> { let mut stdout = stdout(); - stdout.execute(SetForegroundColor(Color::White))?; - stdout.execute(Print(format!( - "Welcome To Ahnlich {}\n\n", - self.client_pool - )))?; - stdout.execute(SetForegroundColor(Color::White))?; + queue!( + stdout, + terminal::Clear(terminal::ClearType::All), + cursor::MoveTo(0, 0), + SetForegroundColor(Color::White), + Print(format!("Welcome To Ahnlich {}\n\n", self.client_pool)), + SetForegroundColor(Color::White), + )?; stdout.flush()?; Ok(()) } @@ -76,13 +93,26 @@ impl Term { } pub(crate) fn format_output(&self, query: &str) -> String { - let output = String::from_iter(query.split(' ').map(|ex| { - if RESERVED_WORDS.contains(&(ex.to_lowercase().as_str())) { - format!("{}", ex.magenta()) - } else { - format!("{}", ex.white()) - } - })); + let matches = |c| c == ';' || c == ' '; + let output = query + .split_inclusive(matches) + .map(|ex| { + // Trim the trailing space or semicolon from the command part + let trimmed_ex = ex.trim_end_matches(matches); + + if self + .client_pool + .commands() + .contains(&(trimmed_ex.to_lowercase().as_str())) + { + // Add back the space or semicolon at the end (if present) + format!("{}{}", trimmed_ex.magenta(), &ex[trimmed_ex.len()..]) + } else { + format!("{}{}", trimmed_ex.white(), &ex[trimmed_ex.len()..]) + } + }) + .collect::(); + output } @@ -131,7 +161,7 @@ impl Term { Ok(()) } - fn read_line(&self, stdout: &mut Stdout) -> io::Result { + fn read_line(&self, stdout: &mut Stdout) -> io::Result { let (start_pos_col, _) = cursor::position()?; let mut output = String::new(); @@ -174,13 +204,23 @@ impl Term { stdout.execute(cursor::MoveToColumn(current_pos_col - 1))?; } } + SpecialEntry::ClrScr => { + queue!( + stdout, + cursor::MoveTo(0, 0), + terminal::Clear(terminal::ClearType::All), + )?; + self.ahnlich_prompt(stdout)?; + self.move_to_pos_and_print(stdout, &output, start_pos_col)?; + } + SpecialEntry::Exit => return Ok(LineResult::Exit), }, Entry::None => { continue; } } } - Ok(output) + Ok(LineResult::Command(output)) } pub async fn run(&self) -> io::Result<()> { @@ -192,34 +232,39 @@ impl Term { loop { self.ahnlich_prompt(&mut stdout)?; let input = self.read_line(&mut stdout)?; - match input.as_str() { - "quit" | "exit" | "exit()" => break, - command => { - let response = self.client_pool.parse_queries(command).await; - - match response { - Ok(success) => { - disable_raw_mode()?; - for msg in success { + match input { + LineResult::Exit => { + break; + } + LineResult::Command(input) => match input.as_str() { + "quit" | "exit" | "exit()" => break, + command => { + let response = self.client_pool.parse_queries(command).await; + + match response { + Ok(success) => { + disable_raw_mode()?; + for msg in success { + queue!( + stdout, + Print(format!("{}\n", msg)), + cursor::MoveToColumn(0) + )?; + } + stdout.flush()?; + enable_raw_mode()? + } + Err(err) => { queue!( stdout, - Print(format!("{}\n", msg)), + Print(format!("{}\n", err.red())), cursor::MoveToColumn(0) )?; + stdout.flush()?; } - stdout.flush()?; - enable_raw_mode()? - } - Err(err) => { - queue!( - stdout, - Print(format!("{}\n", err.red())), - cursor::MoveToColumn(0) - )?; - stdout.flush()?; } } - } + }, }; } disable_raw_mode()?; diff --git a/ahnlich/dsl/src/ai.rs b/ahnlich/dsl/src/ai.rs index 52416675..7bc21ce2 100644 --- a/ahnlich/dsl/src/ai.rs +++ b/ahnlich/dsl/src/ai.rs @@ -42,22 +42,24 @@ fn parse_to_ai_model(input: &str) -> Result { // Parse raw strings separated by ; into a Vec. Examples include but are not restricted // to -// -// PING -// LISTCLIENTS -// LISTSTORES -// INFOSERVER -// PURGESTORES -// DROPSTORE store_name IF EXISTS -// CREATEPREDINDEX (key_1, key_2) in store_name -// DROPPREDINDEX IF EXISTS (key1, key2) in store_name -// CREATENONLINEARALGORITHMINDEX (kdtree) in store_name -// DROPNONLINEARALGORITHMINDEX IF EXISTS (kdtree) in store_name -// DELKEY ([input 1 text], [input 2 text]) IN my_store -// GETPRED ((author = dickens) OR (country != Nigeria)) IN my_store -// GETSIMN 4 WITH [random text inserted here] USING cosinesimilarity IN my_store WHERE (author = dickens) -// CREATESTORE IF NOT EXISTS my_store QUERYMODEL dalle3 INDEXMODEL dalle3 PREDICATES (author, country) NONLINEARALGORITHMINDEX (kdtree) -// SET (([This is the life of Haks paragraphed], {name: Haks, category: dev}), ([This is the life of Deven paragraphed], {name: Deven, category: dev})) in store +pub const COMMANDS: &[&str] = &[ + "ping", + "listclients", + "liststores", + "infoserver", + "purgestores", + "dropstore", // store_name if exists can be handled dynamically + "createpredindex", // (key_1, key_2) in store_name + "droppredindex", // if exists (key1, key2) in store_name + "createnonlinearalgorithmindex", // (kdtree) in store_name + "dropnonlinearalgorithmindex", // if exists (kdtree) in store_name + "delkey", // ([input 1 text], [input 2 text]) in my_store + "getpred", // ((author = dickens) or (country != Nigeria)) in my_store + "getsimn", // 4 with [random text inserted here] using cosinesimilarity in my_store where (author = dickens) + "createstore", // if not exists my_store querymodel dalle3 indexmodel dalle3 predicates (author, country) nonlinearalgorithmindex (kdtree) + "set", // (([This is the life of Haks paragraphed], {name: Haks, category: dev}), ([This is the life of Deven paragraphed], {name: Deven, category: dev})) in store +]; + pub fn parse_ai_query(input: &str) -> Result, DslError> { let pairs = QueryParser::parse(Rule::ai_query, input).map_err(Box::new)?; let statements = pairs.into_iter().collect::>(); diff --git a/ahnlich/dsl/src/db.rs b/ahnlich/dsl/src/db.rs index 9b14f41b..b30a9988 100644 --- a/ahnlich/dsl/src/db.rs +++ b/ahnlich/dsl/src/db.rs @@ -17,22 +17,24 @@ use crate::{error::DslError, predicate::parse_predicate_expression}; // Parse raw strings separated by ; into a Vec. Examples include but are not restricted // to -// -// PING -// LISTCLIENTS -// LISTSTORES -// INFOSERVER -// DROPSTORE store_name IF EXISTS -// CREATEPREDINDEX (key_1, key_2) in store_name -// DROPPREDINDEX IF EXISTS (key1, key2) in store_name -// CREATENONLINEARALGORITHMINDEX (kdtree) in store_name -// DROPNONLINEARALGORITHMINDEX IF EXISTS (kdtree) in store_name -// GETKEY ([1.0, 2.0], [3.0, 4.0]) IN my_store -// DELKEY ([1.2, 3.0], [5.6, 7.8]) IN my_store -// GETPRED ((author = dickens) OR (country != Nigeria)) IN my_store -// GETSIMN 4 WITH [0.65, 2.78] USING cosinesimilarity IN my_store WHERE (author = dickens) -// CREATESTORE IF NOT EXISTS my_store DIMENSION 21 PREDICATES (author, country) NONLINEARALGORITHMINDEX (kdtree) -// SET (([1.0, 2.1, 3.2], {name: Haks, category: dev}), ([3.1, 4.8, 5.0], {name: Deven, category: dev})) in store +pub const COMMANDS: &[&str] = &[ + "ping", + "listclients", + "liststores", + "infoserver", + "dropstore", // store_name if exists can be handled dynamically + "createpredindex", // (key_1, key_2) in store_name + "droppredindex", // if exists (key1, key2) in store_name + "createnonlinearalgorithmindex", // (kdtree) in store_name + "dropnonlinearalgorithmindex", // if exists (kdtree) in store_name + "getkey", // ([1.0, 2.0], [3.0, 4.0]) in my_store + "delkey", // ([1.2, 3.0], [5.6, 7.8]) in my_store + "getpred", // ((author = dickens) or (country != Nigeria)) in my_store + "getsimn", // 4 with [0.65, 2.78] using cosinesimilarity in my_store where (author = dickens) + "createstore", // if not exists my_store dimension 21 predicates (author, country) nonlinearalgorithmindex (kdtree) + "set", // (([1.0, 2.1, 3.2], {name: Haks, category: dev}), ([3.1, 4.8, 5.0], {name: Deven, category: dev})) in store +]; + pub fn parse_db_query(input: &str) -> Result, DslError> { let pairs = QueryParser::parse(Rule::db_query, input).map_err(Box::new)?; let statements = pairs.into_iter().collect::>(); diff --git a/ahnlich/dsl/src/syntax/syntax.pest b/ahnlich/dsl/src/syntax/syntax.pest index 6b8c3e96..e2c598fc 100644 --- a/ahnlich/dsl/src/syntax/syntax.pest +++ b/ahnlich/dsl/src/syntax/syntax.pest @@ -40,12 +40,12 @@ db_statement = _{ invalid_statement } -ping = { whitespace* ~ ^"ping" ~ whitespace* } -info_server = { whitespace* ~ ^"infoserver" ~ whitespace* } -list_stores = { whitespace* ~ ^"liststores" ~ whitespace* } -list_clients = { whitespace* ~ ^"listclients" ~ whitespace* } -purge_stores = { whitespace* ~ ^"purgestores" ~ whitespace* } -drop_store = { whitespace* ~ ^"dropstore" ~ whitespace* ~ store_name ~ (if_exists | invalid_statement)? } +ping = { whitespace* ~ ^"ping" ~ whitespace* ~ !(ASCII_ALPHANUMERIC) } +info_server = { whitespace* ~ ^"infoserver" ~ whitespace* ~ !(ASCII_ALPHANUMERIC)} +list_stores = { whitespace* ~ ^"liststores" ~ whitespace* ~ !(ASCII_ALPHANUMERIC)} +list_clients = { whitespace* ~ ^"listclients" ~ whitespace* ~ !(ASCII_ALPHANUMERIC)} +purge_stores = { whitespace* ~ ^"purgestores" ~ whitespace* ~ !(ASCII_ALPHANUMERIC)} +drop_store = { whitespace* ~ ^"dropstore" ~ whitespace* ~ store_name ~ (if_exists)? ~ !(ASCII_ALPHANUMERIC)} create_pred_index = { whitespace* ~ ^"createpredindex" ~ whitespace* ~ "(" ~ index_names ~ ")" ~ in_ignored ~ store_name } create_non_linear_algorithm_index = { whitespace* ~ ^"createnonlinearalgorithmindex" ~ whitespace* ~ "(" ~ non_linear_algorithms ~ ")" ~ in_ignored ~ store_name} drop_pred_index = { whitespace* ~ ^"droppredindex" ~ whitespace* ~ (if_exists)? ~ "(" ~ index_names ~ ")" ~ in_ignored ~ store_name } From f8b67d7c0ad0f748b38f1e40bc7a2ec1323b706b Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Fri, 4 Oct 2024 03:37:16 +0200 Subject: [PATCH 5/5] Adding history navigation control and typing after cursor move --- ahnlich/Cargo.lock | 49 ++++++++++++++ ahnlich/Cargo.toml | 1 + ahnlich/cli/Cargo.toml | 1 + ahnlich/cli/src/history.rs | 103 +++++++++++++++++++++++++++++ ahnlich/cli/src/lib.rs | 1 + ahnlich/cli/src/term.rs | 50 ++++++++++++-- ahnlich/dsl/src/syntax/syntax.pest | 2 +- 7 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 ahnlich/cli/src/history.rs diff --git a/ahnlich/Cargo.lock b/ahnlich/Cargo.lock index 256b7097..44e12407 100644 --- a/ahnlich/Cargo.lock +++ b/ahnlich/Cargo.lock @@ -502,6 +502,7 @@ dependencies = [ "clap 4.5.4", "crossterm", "deadpool", + "dirs", "dsl", "serde", "serde_json", @@ -761,6 +762,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dsl" version = "0.1.0" @@ -1255,6 +1277,16 @@ version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.5.0", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1525,6 +1557,12 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-float" version = "4.2.0" @@ -1882,6 +1920,17 @@ dependencies = [ "bitflags 2.5.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.4" diff --git a/ahnlich/Cargo.toml b/ahnlich/Cargo.toml index 74fffe36..3cda2cc3 100644 --- a/ahnlich/Cargo.toml +++ b/ahnlich/Cargo.toml @@ -45,3 +45,4 @@ opentelemetry = { version = "0.23.0", features = ["trace"] } tracing-opentelemetry = "0.24.0" log = "0.4" fallible_collections = "0.4.9" +dirs = "5.0.1" diff --git a/ahnlich/cli/Cargo.toml b/ahnlich/cli/Cargo.toml index 4690c2c2..2c5c1e38 100644 --- a/ahnlich/cli/Cargo.toml +++ b/ahnlich/cli/Cargo.toml @@ -15,6 +15,7 @@ path = "src/lib.rs" [dependencies] crossterm = { version = "0.28.1", feature = ["bracketed-paste"]} clap.workspace = true +dirs.workspace = true dsl = { path = "../dsl", version = "*" } thiserror.workspace = true tokio.workspace = true diff --git a/ahnlich/cli/src/history.rs b/ahnlich/cli/src/history.rs new file mode 100644 index 00000000..2a61290a --- /dev/null +++ b/ahnlich/cli/src/history.rs @@ -0,0 +1,103 @@ +use std::{ + fs::OpenOptions, + io::{self, BufRead, Write}, + path::PathBuf, +}; + +fn get_history_file_path() -> PathBuf { + let mut path = dirs::home_dir().expect("Could not find home directory"); + path.push(".ahnlich_cli_history"); + path +} + +fn load_command_history() -> Vec { + let path = get_history_file_path(); + if path.exists() { + let file = OpenOptions::new() + .read(true) + .open(path) + .expect("Unable to open history file"); + let reader = io::BufReader::new(file); + reader.lines().map_while(Result::ok).collect() + } else { + Vec::new() + } +} + +fn save_command_history(commands: &[String]) { + let path = get_history_file_path(); + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path) + .expect("Unable to open history file"); + for command in commands { + writeln!(file, "{}", command).expect("Unable to write to history file"); + } +} + +pub(crate) struct HistoryManager { + command_history: Vec, + current_command_index: usize, +} + +impl HistoryManager { + pub(crate) fn new() -> Self { + let command_history = load_command_history(); + let current_command_index = command_history.len(); + Self { + command_history, + current_command_index, + } + } + + pub(crate) fn down(&mut self) -> String { + if self.current_command_index < self.command_history.len() { + self.current_command_index += 1; + } + if self.is_index_end() { + String::new() + } else { + self.get_at_index() + } + } + + pub(crate) fn up(&mut self) -> String { + if self.current_command_index > 0 { + self.current_command_index -= 1; + } + if self.command_history.is_empty() && self.current_command_index == 0 { + String::new() + } else { + self.get_at_index() + } + } + + fn get_at_index(&self) -> String { + self.command_history[self.current_command_index].clone() + } + + pub(crate) fn reset_index(&mut self) { + self.current_command_index = self.command_history.len(); + } + + pub(crate) fn is_index_end(&self) -> bool { + self.current_command_index == self.command_history.len() + } + + pub(crate) fn save_to_disk(&self) { + save_command_history(&self.command_history); + } + + pub(crate) fn add_command(&mut self, command: &str) { + if let Some(last_command) = self.command_history.last() { + if last_command != command { + self.command_history.push(command.to_string()); + } + } else { + self.command_history.push(command.to_string()); + } + self.reset_index(); + } +} diff --git a/ahnlich/cli/src/lib.rs b/ahnlich/cli/src/lib.rs index dad12301..1bf67e8c 100644 --- a/ahnlich/cli/src/lib.rs +++ b/ahnlich/cli/src/lib.rs @@ -1,3 +1,4 @@ pub mod config; pub mod connect; +mod history; pub mod term; diff --git a/ahnlich/cli/src/term.rs b/ahnlich/cli/src/term.rs index 68ae19b6..b8a50c08 100644 --- a/ahnlich/cli/src/term.rs +++ b/ahnlich/cli/src/term.rs @@ -8,7 +8,7 @@ use crossterm::{ }; use std::io::{self, stdout, Stdout, Write}; -use crate::connect::AgentPool; +use crate::{connect::AgentPool, history::HistoryManager}; #[derive(Debug)] enum SpecialEntry { @@ -29,6 +29,15 @@ enum Entry { None, } +impl Entry { + fn is_history_key(&self) -> bool { + matches!( + self, + Entry::Special(SpecialEntry::Up) | Entry::Special(SpecialEntry::Down) + ) + } +} + #[derive(Debug)] enum LineResult { Command(String), @@ -136,6 +145,7 @@ impl Term { queue!( stdout, cursor::MoveToColumn(col_pos), + terminal::Clear(terminal::ClearType::FromCursorDown), Print(formatted_output) )?; stdout.flush()?; @@ -161,21 +171,45 @@ impl Term { Ok(()) } - fn read_line(&self, stdout: &mut Stdout) -> io::Result { + fn read_line( + &self, + stdout: &mut Stdout, + history: &mut HistoryManager, + ) -> io::Result { let (start_pos_col, _) = cursor::position()?; let mut output = String::new(); loop { let char = self.read_char()?; let (current_pos_col, _) = cursor::position()?; + if !char.is_history_key() { + history.reset_index(); + } match char { Entry::Char(c) => { - output.push(c); + let insertion_position = current_pos_col - start_pos_col; + output.insert(insertion_position as usize, c); self.move_to_pos_and_print(stdout, &output, start_pos_col)?; + stdout.execute(cursor::MoveToColumn(current_pos_col + 1))?; } Entry::Special(special) => match special { - SpecialEntry::Up | SpecialEntry::Down => { - continue; + SpecialEntry::Up => { + output = history.up(); + queue!( + stdout, + cursor::MoveToColumn(start_pos_col), + terminal::Clear(terminal::ClearType::FromCursorDown), + )?; + self.move_to_pos_and_print(stdout, &output, start_pos_col)?; + } + SpecialEntry::Down => { + output = history.down(); + queue!( + stdout, + cursor::MoveToColumn(start_pos_col), + terminal::Clear(terminal::ClearType::FromCursorDown), + )?; + self.move_to_pos_and_print(stdout, &output, start_pos_col)?; } SpecialEntry::Enter => { queue!(stdout, Print("\n"), cursor::MoveToColumn(0))?; @@ -220,6 +254,8 @@ impl Term { } } } + history.add_command(&output); + history.save_to_disk(); Ok(LineResult::Command(output)) } @@ -229,9 +265,11 @@ impl Term { stdout.execute(cursor::EnableBlinking)?; stdout.execute(cursor::SetCursorStyle::BlinkingBar)?; + let mut history = HistoryManager::new(); + loop { self.ahnlich_prompt(&mut stdout)?; - let input = self.read_line(&mut stdout)?; + let input = self.read_line(&mut stdout, &mut history)?; match input { LineResult::Exit => { break; diff --git a/ahnlich/dsl/src/syntax/syntax.pest b/ahnlich/dsl/src/syntax/syntax.pest index e2c598fc..96d7e113 100644 --- a/ahnlich/dsl/src/syntax/syntax.pest +++ b/ahnlich/dsl/src/syntax/syntax.pest @@ -45,7 +45,7 @@ info_server = { whitespace* ~ ^"infoserver" ~ whitespace* ~ !(ASCII_ALPHANUMERIC list_stores = { whitespace* ~ ^"liststores" ~ whitespace* ~ !(ASCII_ALPHANUMERIC)} list_clients = { whitespace* ~ ^"listclients" ~ whitespace* ~ !(ASCII_ALPHANUMERIC)} purge_stores = { whitespace* ~ ^"purgestores" ~ whitespace* ~ !(ASCII_ALPHANUMERIC)} -drop_store = { whitespace* ~ ^"dropstore" ~ whitespace* ~ store_name ~ (if_exists)? ~ !(ASCII_ALPHANUMERIC)} +drop_store = { whitespace* ~ ^"dropstore" ~ whitespace* ~ store_name ~ (if_exists | invalid_statement)?} create_pred_index = { whitespace* ~ ^"createpredindex" ~ whitespace* ~ "(" ~ index_names ~ ")" ~ in_ignored ~ store_name } create_non_linear_algorithm_index = { whitespace* ~ ^"createnonlinearalgorithmindex" ~ whitespace* ~ "(" ~ non_linear_algorithms ~ ")" ~ in_ignored ~ store_name} drop_pred_index = { whitespace* ~ ^"droppredindex" ~ whitespace* ~ (if_exists)? ~ "(" ~ index_names ~ ")" ~ in_ignored ~ store_name }