diff options
author | Blaž Hrastnik | 2020-05-20 09:14:51 +0000 |
---|---|---|
committer | Blaž Hrastnik | 2020-05-20 09:14:51 +0000 |
commit | 240e5f4e3d27415b792776dd126d15302d53e83b (patch) | |
tree | 8579f77ca5d200bafd7df09ab93ceb65e5bdce3b /helix-term |
Initial import.
Diffstat (limited to 'helix-term')
-rw-r--r-- | helix-term/.gitignore | 1 | ||||
-rw-r--r-- | helix-term/Cargo.toml | 19 | ||||
-rw-r--r-- | helix-term/README.md | 7 | ||||
-rw-r--r-- | helix-term/src/line.rs | 111 | ||||
-rw-r--r-- | helix-term/src/main.rs | 227 |
5 files changed, 365 insertions, 0 deletions
diff --git a/helix-term/.gitignore b/helix-term/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/helix-term/.gitignore @@ -0,0 +1 @@ +/target diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml new file mode 100644 index 00000000..f6528034 --- /dev/null +++ b/helix-term/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "helix-term" +version = "0.1.0" +authors = ["Blaž Hrastnik <blaz@mxxn.io>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[[bin]] +name = "hx" +path = "src/main.rs" + +# [[bin]] +# name = "line" +# path = "src/line.rs" + +[dependencies] +termwiz = { git = "https://github.com/wez/wezterm", features = ["widgets"] } +anyhow = "1.0.31" diff --git a/helix-term/README.md b/helix-term/README.md new file mode 100644 index 00000000..abefa6fd --- /dev/null +++ b/helix-term/README.md @@ -0,0 +1,7 @@ + +window -> buffer -> text +\-> contains "view", a viewport into the buffer + +view +\-> selections etc +-> cursor diff --git a/helix-term/src/line.rs b/helix-term/src/line.rs new file mode 100644 index 00000000..90fb5e6f --- /dev/null +++ b/helix-term/src/line.rs @@ -0,0 +1,111 @@ +use termwiz::cell::AttributeChange; +use termwiz::color::{AnsiColor, ColorAttribute, RgbColor}; +use termwiz::lineedit::*; + +#[derive(Default)] +struct Host { + history: BasicHistory, +} + +impl LineEditorHost for Host { + // Render the prompt with a darkslateblue background color if + // the terminal supports true color, otherwise render it with + // a navy blue ansi color. + fn render_prompt(&self, prompt: &str) -> Vec<OutputElement> { + vec![ + OutputElement::Attribute(AttributeChange::Background( + ColorAttribute::TrueColorWithPaletteFallback( + RgbColor::from_named("darkslateblue").unwrap(), + AnsiColor::Navy.into(), + ), + )), + OutputElement::Text(prompt.to_owned()), + ] + } + + fn history(&mut self) -> &mut dyn History { + &mut self.history + } + + /// Demo of the completion API for words starting with "h" or "he" + fn complete(&self, line: &str, cursor_position: usize) -> Vec<CompletionCandidate> { + let mut candidates = vec![]; + if let Some((range, word)) = word_at_cursor(line, cursor_position) { + let words = &["hello", "help", "he-man"]; + + for w in words { + if w.starts_with(word) { + candidates.push(CompletionCandidate { + range: range.clone(), + text: w.to_string(), + }); + } + } + } + candidates + } +} + +/// This is a conceptually simple function that computes the bounds +/// of the whitespace delimited word at the specified cursor position +/// in the supplied line string. +/// It returns the range and the corresponding slice out of the line. +/// This function is sufficient for example purposes; in a real application +/// the equivalent function would need to be aware of quoting and other +/// application specific context. +fn word_at_cursor(line: &str, cursor_position: usize) -> Option<(std::ops::Range<usize>, &str)> { + let char_indices: Vec<(usize, char)> = line.char_indices().collect(); + if char_indices.is_empty() { + return None; + } + let char_position = char_indices + .iter() + .position(|(idx, _)| *idx == cursor_position) + .unwrap_or(char_indices.len()); + + // Look back until we find whitespace + let mut start_position = char_position; + while start_position > 0 + && start_position <= char_indices.len() + && !char_indices[start_position - 1].1.is_whitespace() + { + start_position -= 1; + } + + // Look forwards until we find whitespace + let mut end_position = char_position; + while end_position < char_indices.len() && !char_indices[end_position].1.is_whitespace() { + end_position += 1; + } + + if end_position > start_position { + let range = char_indices[start_position].0 + ..char_indices + .get(end_position) + .map(|c| c.0 + 1) + .unwrap_or(line.len()); + Some((range.clone(), &line[range])) + } else { + None + } +} + +fn main() -> anyhow::Result<()> { + println!("Type `exit` to quit this example, or start a word with `h` and press Tab."); + let mut terminal = line_editor_terminal()?; + let mut editor = LineEditor::new(&mut terminal); + + let mut host = Host::default(); + loop { + if let Some(line) = editor.read_line(&mut host)? { + println!("read line: {:?}", line); + if line == "exit" { + break; + } + + host.history().add(&line); + } + } + + Ok(()) +} diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs new file mode 100644 index 00000000..e13309eb --- /dev/null +++ b/helix-term/src/main.rs @@ -0,0 +1,227 @@ +//! This example shows how to make a basic widget that accumulates +//! text input and renders it to the screen +#![allow(unused)] +use anyhow::Error; +use termwiz::caps::Capabilities; +use termwiz::cell::AttributeChange; +use termwiz::color::{AnsiColor, ColorAttribute, RgbColor}; +use termwiz::input::*; +use termwiz::surface::Change; +use termwiz::terminal::buffered::BufferedTerminal; +use termwiz::terminal::{new_terminal, Terminal}; +use termwiz::widgets::*; + +/// This is a widget for our application +struct MainScreen {} + +impl MainScreen { + pub fn new() -> Self { + Self {} + } +} + +impl Widget for MainScreen { + fn process_event(&mut self, event: &WidgetEvent, _args: &mut UpdateArgs) -> bool { + true // handled it all + } + + /// Draw ourselves into the surface provided by RenderArgs + fn render(&mut self, args: &mut RenderArgs) { + // args.surface.add_change(Change::ClearScreen( + // ColorAttribute::TrueColorWithPaletteFallback( + // RgbColor::new(0x31, 0x1B, 0x92), + // AnsiColor::Black.into(), + // ), + // )); + // args.surface + // .add_change(Change::Attribute(AttributeChange::Foreground( + // ColorAttribute::TrueColorWithPaletteFallback( + // RgbColor::new(0xB3, 0x88, 0xFF), + // AnsiColor::Purple.into(), + // ), + // ))); + } + + fn get_size_constraints(&self) -> layout::Constraints { + let mut constraints = layout::Constraints::default(); + constraints.child_orientation = layout::ChildOrientation::Vertical; + constraints + } +} + +struct Buffer<'a> { + /// Holds the input text that we wish the widget to display + text: &'a mut String, +} + +impl<'a> Buffer<'a> { + /// Initialize the widget with the input text + pub fn new(text: &'a mut String) -> Self { + Self { text } + } +} + +impl<'a> Widget for Buffer<'a> { + fn process_event(&mut self, event: &WidgetEvent, _args: &mut UpdateArgs) -> bool { + match event { + WidgetEvent::Input(InputEvent::Key(KeyEvent { + key: KeyCode::Char(c), + .. + })) => self.text.push(*c), + WidgetEvent::Input(InputEvent::Key(KeyEvent { + key: KeyCode::Enter, + .. + })) => { + self.text.push_str("\r\n"); + } + WidgetEvent::Input(InputEvent::Paste(s)) => { + self.text.push_str(&s); + } + _ => {} + } + + true // handled it all + } + + /// Draw ourselves into the surface provided by RenderArgs + fn render(&mut self, args: &mut RenderArgs) { + args.surface + .add_change(Change::ClearScreen(ColorAttribute::Default)); + + // args.surface + // .add_change(Change::Attribute(AttributeChange::Foreground( + // ColorAttribute::TrueColorWithPaletteFallback( + // RgbColor::new(0x11, 0x00, 0xFF), + // AnsiColor::Purple.into(), + // ), + // ))); + let dims = args.surface.dimensions(); + args.surface + .add_change(format!("🤷 surface size is {:?}\r\n", dims)); + args.surface.add_change(self.text.clone()); + + // Place the cursor at the end of the text. + // A more advanced text editing widget would manage the + // cursor position differently. + *args.cursor = CursorShapeAndPosition { + coords: args.surface.cursor_position().into(), + shape: termwiz::surface::CursorShape::SteadyBar, + ..Default::default() + }; + } + + fn get_size_constraints(&self) -> layout::Constraints { + let mut c = layout::Constraints::default(); + c.set_valign(layout::VerticalAlignment::Top); + c + } +} + +struct StatusLine {} + +impl StatusLine { + pub fn new() -> Self { + StatusLine {} + } +} +impl Widget for StatusLine { + fn process_event(&mut self, event: &WidgetEvent, _args: &mut UpdateArgs) -> bool { + true + } + + fn render(&mut self, args: &mut RenderArgs) { + args.surface.add_change(Change::ClearScreen( + ColorAttribute::TrueColorWithPaletteFallback( + RgbColor::new(0xFF, 0xFF, 0xFF), + AnsiColor::Black.into(), + ), + )); + args.surface + .add_change(Change::Attribute(AttributeChange::Foreground( + ColorAttribute::TrueColorWithPaletteFallback( + RgbColor::new(0x00, 0x00, 0x00), + AnsiColor::Black.into(), + ), + ))); + + args.surface.add_change(" helix"); + } + + fn get_size_constraints(&self) -> layout::Constraints { + *layout::Constraints::default() + .set_fixed_height(1) + .set_valign(layout::VerticalAlignment::Bottom) + } +} + +fn main() -> Result<(), Error> { + // Start with an empty string; typing into the app will + // update this string. + let mut typed_text = String::new(); + + { + // Create a terminal and put it into full screen raw mode + let caps = Capabilities::new_from_env()?; + let mut buf = BufferedTerminal::new(new_terminal(caps)?)?; + buf.terminal().enter_alternate_screen()?; + buf.terminal().set_raw_mode()?; + + // Set up the UI + let mut ui = Ui::new(); + + let root_id = ui.set_root(MainScreen::new()); + let buffer_id = ui.add_child(root_id, Buffer::new(&mut typed_text)); + // let root_id = ui.set_root(Buffer::new(&mut typed_text)); + ui.add_child(root_id, StatusLine::new()); + ui.set_focus(buffer_id); + + loop { + ui.process_event_queue()?; + + // After updating and processing all of the widgets, compose them + // and render them to the screen. + if ui.render_to_screen(&mut buf)? { + // We have more events to process immediately; don't block waiting + // for input below, but jump to the top of the loop to re-run the + // updates. + continue; + } + // Compute an optimized delta to apply to the terminal and display it + buf.flush()?; + + // Wait for user input + match buf.terminal().poll_input(None) { + Ok(Some(InputEvent::Resized { rows, cols })) => { + // FIXME: this is working around a bug where we don't realize + // that we should redraw everything on resize in BufferedTerminal. + buf.add_change(Change::ClearScreen(Default::default())); + buf.resize(cols, rows); + } + Ok(Some(input)) => match input { + InputEvent::Key(KeyEvent { + key: KeyCode::Escape, + .. + }) => { + // Quit the app when escape is pressed + break; + } + input @ _ => { + // Feed input into the Ui + ui.queue_event(WidgetEvent::Input(input)); + } + }, + Ok(None) => {} + Err(e) => { + print!("{:?}\r\n", e); + break; + } + } + } + } + + // After we've stopped the full screen raw terminal, + // print out the final edited value of the input text. + println!("The text you entered: {}", typed_text); + + Ok(()) +} |