diff --git a/.gitmodules b/.gitmodules index 8a9da62..f38dda8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "lib/tui-textarea"] path = lib/tui-textarea url = https://github.com/cosmikwolf/tui-textarea +[submodule "lib/ansi-to-tui"] + path = lib/ansi-to-tui + url = https://github.com/cosmikwolf/ansi-to-tui +[submodule "lib/bat"] + path = lib/bat + url = git@github.com:sharkdp/bat.git diff --git a/Cargo.lock b/Cargo.lock index 09e855a..b287e98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi-to-tui" +version = "3.1.0" +dependencies = [ + "nom", + "ratatui 0.24.0", + "thiserror", +] + [[package]] name = "ansi-to-tui" version = "3.1.0" @@ -514,6 +523,12 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -763,6 +778,28 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +[[package]] +name = "clipboard" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "x11-clipboard", +] + +[[package]] +name = "clipboard-win" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b" +dependencies = [ + "winapi", +] + [[package]] name = "clircle" version = "0.4.0" @@ -2397,6 +2434,15 @@ dependencies = [ "hashbrown 0.14.2", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.1.0" @@ -2688,6 +2734,35 @@ dependencies = [ "libc", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "object" version = "0.32.1" @@ -3725,7 +3800,7 @@ dependencies = [ name = "sazid" version = "0.1.0" dependencies = [ - "ansi-to-tui", + "ansi-to-tui 3.1.0", "anstyle", "async-openai 0.16.3", "async-recursion", @@ -3737,6 +3812,7 @@ dependencies = [ "bwrap", "chrono", "clap 4.4.8", + "clipboard", "color-eyre", "config", "console-subscriber", @@ -4941,8 +5017,10 @@ dependencies = [ name = "tui-textarea" version = "0.4.0" dependencies = [ - "ansi-to-tui", + "ansi-to-tui 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bat", "crossterm 0.27.0", + "nu-ansi-term 0.49.0", "ratatui 0.24.0", "tracing", "unicode-width", @@ -5511,6 +5589,25 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "x11-clipboard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea" +dependencies = [ + "xcb", +] + +[[package]] +name = "xcb" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" +dependencies = [ + "libc", + "log", +] + [[package]] name = "xmlparser" version = "0.13.6" diff --git a/Cargo.toml b/Cargo.toml index f66cb09..40ca605 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ description = "Semantic GPT Programming Intelligence" repository = "https://github.com/cosmikwolf/sazid" authors = ["tenkai "] default-run = "sazid" - +resolver = "2" [[bin]] name = "sazid" path = "src/main.rs" @@ -21,8 +21,12 @@ path = "bin/crossterm_character_test.rs" [dependencies] uuid = "1.5.0" -ansi-to-tui = { version = "3.1.0" } -tui-textarea = { version = "0.4.0", path = "./lib/tui-textarea" } +ansi-to-tui = { path = "./lib/ansi-to-tui" } +tui-textarea = { path = "./lib/tui-textarea", features = [ + "crossterm", + "ratatui", + "ansi-escapes", +] } bwrap = { version = "1.3.0", features = ["use_std"] } async-openai = "0.16.3" async-recursion = "1.0.5" @@ -115,6 +119,7 @@ dsync = { version = "0.0.16", features = ["async"] } tree-sitter = "0.20.10" tree-sitter-rust = "0.20.4" rust-sitter = "0.4.1" +clipboard = "0.5.0" [dev-dependencies] insta = { version = "1.34.0", features = [ diff --git a/lib/ansi-to-tui b/lib/ansi-to-tui new file mode 160000 index 0000000..cc377fc --- /dev/null +++ b/lib/ansi-to-tui @@ -0,0 +1 @@ +Subproject commit cc377fcae5787c530f5d2077ed8806531e35c455 diff --git a/lib/tui-textarea b/lib/tui-textarea index cdc5779..2f7e2a2 160000 --- a/lib/tui-textarea +++ b/lib/tui-textarea @@ -1 +1 @@ -Subproject commit cdc57797222e5106f42b1491e62bbb7661aa0630 +Subproject commit 2f7e2a23a4145d0186b11705e1d4c10da8ac240a diff --git a/src/app/session_view.rs b/src/app/session_view.rs index 4dd6cb1..6433ffa 100644 --- a/src/app/session_view.rs +++ b/src/app/session_view.rs @@ -1,13 +1,22 @@ -use bat::{assets::HighlightingAssets, config::Config, controller::Controller, style::StyleComponents, Input}; +use bat::{assets::HighlightingAssets, config::Config, controller::Controller, style::StyleComponents}; use color_eyre::owo_colors::OwoColorize; +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use crossterm::event::KeyModifiers; use nu_ansi_term::Color::*; use nu_ansi_term::Style; +use ratatui::style::Modifier; +use ratatui::widgets::Block; +use ratatui::widgets::Borders; use std::default::Default; +use std::path::Path; use textwrap::{self, Options, WordSeparator, WordSplitter, WrapAlgorithm}; -use tui_textarea::TextArea; +use crate::action::Action; use crate::trace_dbg; +use tui_textarea::{CursorMove, Input, Key, Scrolling, TextArea}; +use super::errors::SazidError; use super::{messages::MessageContainer, session_data::SessionData}; use ropey::Rope; @@ -24,13 +33,36 @@ pub struct SessionView<'a> { } impl<'a> SessionView<'a> { + pub fn unfocus_textarea(&mut self) { + use ratatui::style::{Color, Style}; + self.text_area.set_cursor_line_style(Style::default()); + self.text_area.set_cursor_style(Style::default()); + self.text_area.set_block( + Block::default() + .borders(Borders::ALL) + .style(Style::default().fg(Color::DarkGray)) + .title(" Inactive (^X to switch) "), + ); + } + + pub fn focus_textarea(&mut self) { + use ratatui::style::{Color, Style}; + self.text_area.move_cursor(CursorMove::Top); + self.text_area.move_cursor(CursorMove::Head); + self + .text_area + .set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED).add_modifier(Modifier::SLOW_BLINK)); + self.text_area.set_cursor_style(Style::default().bg(Color::Yellow)); + self.text_area.set_block(Block::default().borders(Borders::ALL).style(Style::default()).title(" Active ")); + } + pub fn set_window_width(&mut self, width: usize, _messages: &mut [MessageContainer]) { let new_value = width - 6; if self.window_width != new_value { trace_dbg!("setting window width to {}", new_value); self.window_width = new_value; - self.renderer.config.term_width = 10000; + self.renderer.config.term_width = new_value; //self.renderer.config.term_width = new_value; } } @@ -74,7 +106,7 @@ impl<'a> SessionView<'a> { let left_padding = self.window_width.saturating_sub(text_width) / 2; trace_dbg!("left_padding: {}\ttext_width: {}, window_width: {}", left_padding, text_width, self.window_width); let stylized = self.renderer.render_message_bat(format!("{}", &message).as_str()); - let options = Options::new(text_width) + let options = Options::new(text_width-10) //.break_words(false) .word_splitter(WordSplitter::NoHyphenation) .word_separator(WordSeparator::AsciiSpace) @@ -104,7 +136,10 @@ impl<'a> SessionView<'a> { self.new_data = true; self.text_area.replace_at_end(message.stylized.to_string(), original_message_length); - trace_dbg!("stylized {}\n\n{}", message.stylized.red(), self.rendered_text.len_chars()); + + message.stylized.to_string().lines().for_each(|l| { + trace_dbg!("line: {:#?}", l); + }); self.rendered_text.remove(rendered_text_message_start_index..); self.rendered_text.append(message.stylized.clone()); @@ -149,7 +184,7 @@ impl<'a> BatRenderer<'a> { ]); let config: Config<'static> = Config { colored_output: true, - language: Some("markdown"), + language: Some("Markdown Extended"), style_components, show_nonprintable: false, tab_width: 2, @@ -161,7 +196,8 @@ impl<'a> BatRenderer<'a> { use_custom_assets: true, ..Default::default() }; - let assets = HighlightingAssets::from_binary(); + // let assets = HighlightingAssets::from_binary(); + let assets = HighlightingAssets::from_cache(&Path::new("./lib/bat/assets")).unwrap(); let _buffer: Vec = Vec::new(); BatRenderer { config, assets } } @@ -181,7 +217,7 @@ impl<'a> BatRenderer<'a> { //trace_dbg!("render_message_bat: {:?}", self.rendered_bytes); //text.bytes().skip(self.buffer.len()).for_each(|b| self.buffer.push(b)); - let input = Input::from_bytes(text.as_bytes()); + let input = bat::Input::from_bytes(text.as_bytes()); //trace_dbg!("render_message_bat: {:?}", self.rendered_bytes); controller.run_with_error_handler(vec![input.into()], Some(&mut buffer), Self::render_error).unwrap(); //trace_dbg!("render_message_bat: {:?}", buffer); diff --git a/src/components/home.rs b/src/components/home.rs index 8e8cb28..7807b59 100644 --- a/src/components/home.rs +++ b/src/components/home.rs @@ -115,20 +115,16 @@ impl Component for Home<'static> { }, Action::EnterCommand => { self.mode = Mode::Command; - self.session.mode = Mode::Command; }, Action::EnterNormal => { self.mode = Mode::Normal; - self.session.mode = Mode::Normal; }, Action::EnterVisual => { self.mode = Mode::Visual; - self.session.mode = Mode::Visual; }, Action::EnterInsert => { trace_dbg!("enter insert mode"); self.mode = Mode::Insert; - self.session.mode = Mode::Insert; }, Action::CommandResult(result) => { self.replace_input(result); @@ -137,12 +133,10 @@ impl Component for Home<'static> { Action::EnterProcessing => { self.clear_input(); self.mode = Mode::Processing; - self.session.mode = Mode::Processing; }, Action::ExitProcessing => { // TODO: Make this go to previous mode instead self.mode = Mode::Normal; - self.session.mode = Mode::Normal; }, _ => (), } diff --git a/src/components/session.rs b/src/components/session.rs index 8f53217..5cda721 100644 --- a/src/components/session.rs +++ b/src/components/session.rs @@ -4,7 +4,9 @@ use async_openai::types::{ ChatCompletionRequestUserMessageContent, CreateChatCompletionRequest, CreateEmbeddingRequestArgs, CreateEmbeddingResponse, Role, }; +use clipboard::{ClipboardContext, ClipboardProvider}; use color_eyre::owo_colors::OwoColorize; +use crossterm::event::KeyModifiers; use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind}; use futures::StreamExt; use nu_ansi_term::Color::*; @@ -18,6 +20,7 @@ use std::result::Result; use std::{fs, io}; use tokio::sync::mpsc::UnboundedSender; use tui_textarea::TextArea; +use tui_textarea::{CursorMove, Scrolling}; use async_openai::{config::OpenAIConfig, Client}; @@ -130,7 +133,7 @@ impl Component for Session<'static> { "- you write full code when requested", "- your responses are conscise and terse", "- Use the functions available to execute with the user inquiry.", - "- Provide your responses as markdown formatted text.", + "- Provide ==your responses== as markdown formatted text.", "- Make sure to properly entabulate any code blocks", "- Do not try and execute arbitrary python code.", "- Do not try to infer a path to a file, if you have not been provided a path with the root ./, use the file_search function to verify the file path before you execute a function call.", @@ -190,6 +193,30 @@ impl Component for Session<'static> { Action::SetInputVsize(vsize) => { self.input_vsize = vsize; }, + Action::EnterCommand => { + self.view.unfocus_textarea(); + self.mode = Mode::Command; + }, + Action::EnterNormal => { + self.view.focus_textarea(); + self.mode = Mode::Normal; + }, + Action::EnterVisual => { + self.view.unfocus_textarea(); + self.mode = Mode::Visual; + }, + Action::EnterInsert => { + self.view.unfocus_textarea(); + self.mode = Mode::Insert; + }, + Action::EnterProcessing => { + self.view.unfocus_textarea(); + self.mode = Mode::Processing; + }, + Action::ExitProcessing => { + self.view.focus_textarea(); + self.mode = Mode::Normal; + }, _ => (), } //self.action_tx.clone().unwrap().send(Action::Render).unwrap(); @@ -231,14 +258,88 @@ impl Component for Session<'static> { fn handle_key_events(&mut self, key: KeyEvent) -> Result, SazidError> { self.last_events.push(key); - match self.mode { - Mode::Normal => match key.code { - KeyCode::Char('j') => self.scroll_down(), - KeyCode::Char('k') => self.scroll_up(), - _ => Ok(None), + Ok(match self.mode { + Mode::Normal => match key { + KeyEvent { code: KeyCode::Char('d'), modifiers: KeyModifiers::CONTROL, .. } => { + self.view.text_area.scroll(Scrolling::HalfPageDown); + Some(Action::Update) + }, + KeyEvent { code: KeyCode::Char('u'), modifiers: KeyModifiers::CONTROL, .. } => { + self.view.text_area.scroll(Scrolling::HalfPageUp); + Some(Action::Update) + }, + KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::CONTROL, .. } => { + self.view.text_area.scroll(Scrolling::PageDown); + Some(Action::Update) + }, + KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::CONTROL, .. } => { + self.view.text_area.scroll(Scrolling::PageUp); + Some(Action::Update) + }, + KeyEvent { code: KeyCode::Char('h'), .. } => { + self.view.text_area.move_cursor(CursorMove::Back); + Some(Action::Update) + }, + KeyEvent { code: KeyCode::Char('j'), .. } => { + self.view.text_area.move_cursor(CursorMove::Down); + trace_dbg!("cursor: {:#?}", self.view.text_area.cursor()); + Some(Action::Update) + }, + KeyEvent { code: KeyCode::Char('k'), .. } => { + self.view.text_area.move_cursor(CursorMove::Up); + trace_dbg!("cursor: {:#?}", self.view.text_area.cursor()); + Some(Action::Update) + }, + KeyEvent { code: KeyCode::Char('l'), .. } => { + self.view.text_area.move_cursor(CursorMove::Forward); + Some(Action::Update) + }, + KeyEvent { code: KeyCode::Char('w'), .. } => { + self.view.text_area.move_cursor(CursorMove::WordForward); + Some(Action::Update) + }, + KeyEvent { code: KeyCode::Char('b'), .. } => { + self.view.text_area.move_cursor(CursorMove::WordBack); + Some(Action::Update) + }, + KeyEvent { code: KeyCode::Char('^'), .. } => { + self.view.text_area.move_cursor(CursorMove::Head); + Some(Action::Update) + }, + KeyEvent { code: KeyCode::Char('$'), .. } => { + self.view.text_area.move_cursor(CursorMove::End); + Some(Action::Update) + }, + KeyEvent { code: KeyCode::Char('v'), .. } => { + self.view.text_area.start_selection(); + Some(Action::Update) + }, + KeyEvent { code: KeyCode::Char('y'), .. } => { + self.view.text_area.copy(); + let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); + ctx.set_contents(self.view.text_area.yank_text()).unwrap(); + Some(Action::Update) + }, + KeyEvent { code: KeyCode::Esc, .. } => { + self.view.text_area.cancel_selection(); + Some(Action::Update) + }, + KeyEvent { code: KeyCode::Char('V'), modifiers: KeyModifiers::SHIFT, .. } => { + self.view.text_area.start_selection(); + self.view.text_area.move_cursor(CursorMove::Head); + self.view.text_area.start_selection(); + self.view.text_area.move_cursor(CursorMove::End); + Some(Action::Update) + }, + _ => None, }, - _ => Ok(None), - } + _ => None, + // KeyCode::Char('j') => self.scroll_down(), + // KeyCode::Char('k') => self.scroll_up(), + // _ => Ok(None), + // }, + // _ => Ok(None), + }) } fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<(), SazidError> { diff --git a/src/utils.rs b/src/utils.rs index 76ddec8..a4debf2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -141,7 +141,7 @@ macro_rules! trace_dbg { match $ex { value => { //tracing::event!(target: $target, $level, ?value, ?formatted); - tracing::event!(target: $target, $level, %value, stringify!($ex)); + tracing::event!(target: $target, $level, ?value, stringify!($ex)); //tracing::event!(target: $target, $level, ?value, stringify!($ex)); value }