├── .gitignore ├── img ├── smith.png ├── screenshot.png └── smith.svg ├── assets ├── gruvbox.themedump └── gruvbox.tmTheme ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── data ├── mod.rs ├── select │ └── mod.rs ├── record │ ├── action.rs │ └── mod.rs └── text.rs ├── main.rs ├── view ├── screen.rs └── mod.rs └── command └── mod.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /img/smith.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IGI-111/Smith/HEAD/img/smith.png -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IGI-111/Smith/HEAD/img/screenshot.png -------------------------------------------------------------------------------- /assets/gruvbox.themedump: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IGI-111/Smith/HEAD/assets/gruvbox.themedump -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | cache: cargo 3 | rust: 4 | - stable 5 | - beta 6 | - nightly 7 | 8 | before_install: 9 | - sudo apt-get install -qq xorg-dev libxcb-render-util0-dev libxcb-shape0-dev libxcb-xfixes0-dev 10 | 11 | script: 12 | - cargo build --verbose 13 | - cargo build --release --verbose 14 | 15 | matrix: 16 | include: 17 | env: FMT 18 | rust: stable 19 | install: 20 | - rustup component add rustfmt-preview 21 | script: 22 | - cargo fmt -- --check 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "smith" 3 | version = "2.0.1" 4 | authors = ["IGI-111 "] 5 | description = "Smith is a simple terminal-based text editor written in Rust." 6 | repository = "https://github.com/IGI-111/Smith" 7 | keywords = [ "text", "editor", "terminal", "termion" ] 8 | license = "MIT" 9 | edition = "2018" 10 | 11 | [dependencies] 12 | clipboard = "0.5.0" 13 | ropey = "1.1.0" 14 | termion = "1.5.5" 15 | unicode-width = "0.1.7" 16 | unicode-segmentation = "1.6.0" 17 | delegate-attr = "0.2.0" 18 | syntect = "3.3.0" 19 | ndarray = "0.13.0" 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Jibril Saffi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Smith 4 | 5 |

6 | 7 | Crate status 8 | Build status 9 | 10 | Smith is a simple terminal-based text editor written in Rust. 11 | 12 | ## Install 13 | 14 | Using Cargo: 15 | ``` 16 | cargo install smith 17 | ``` 18 | 19 | To compile Smith with clipboard support on Ubuntu, you may need to install some libraries: 20 | ``` 21 | sudo apt-get install -qq xorg-dev libxcb-render-util0-dev libxcb-shape0-dev libxcb-xfixes0-dev 22 | ``` 23 | 24 | 25 | ## Features 26 | 27 | * line numbers 28 | * syntax highlighting 29 | * undo/redo 30 | * standard keybindings (Ctrl-S, Ctrl-Z, Ctrl-C, Esc...) 31 | * mouse support 32 | * clipboard support 33 | 34 | With more planned such as user configurations, search & replace, persistent undo, etc. 35 | 36 | Here's what it looks like editing its own source code: 37 | 38 |

39 | Smith in action 40 |

41 | -------------------------------------------------------------------------------- /src/data/mod.rs: -------------------------------------------------------------------------------- 1 | mod record; 2 | mod select; 3 | mod text; 4 | 5 | pub use self::record::Recorded; 6 | pub use self::record::Undoable; 7 | pub use self::select::{Select, Selectable}; 8 | pub use self::text::Text; 9 | 10 | use std::io::Result; 11 | 12 | pub trait Editable { 13 | fn step(&mut self, mov: Movement); 14 | fn move_to(&mut self, pos: usize); 15 | fn move_at(&mut self, line: usize, col: usize); 16 | fn insert(&mut self, c: char); 17 | fn insert_forward(&mut self, c: char); 18 | fn delete(&mut self) -> Option; 19 | fn delete_forward(&mut self) -> Option; 20 | fn pos(&self) -> usize; 21 | fn line(&self) -> usize; 22 | fn col(&self) -> usize; 23 | fn line_count(&self) -> usize; 24 | fn len(&self) -> usize; 25 | fn iter(&self) -> CharIter; 26 | fn lines(&self) -> LineIter; 27 | fn iter_line(&self, line: usize) -> CharIter; 28 | fn line_index_to_char_index(&self, line: usize) -> usize; 29 | } 30 | 31 | pub type CharIter<'a> = ropey::iter::Chars<'a>; 32 | pub type LineIter<'a> = ropey::iter::Lines<'a>; 33 | 34 | pub trait Named { 35 | fn name(&self) -> &String; 36 | fn set_name(&mut self, name: String); 37 | } 38 | 39 | pub trait Saveable: Named { 40 | fn save(&mut self) -> Result<()>; 41 | } 42 | 43 | pub trait Modifiable: Editable { 44 | fn was_modified(&self) -> bool; 45 | } 46 | 47 | #[derive(Clone)] 48 | pub enum Movement { 49 | Up, 50 | Down, 51 | Left, 52 | Right, 53 | LineStart, 54 | LineEnd, 55 | PageUp(usize), 56 | PageDown(usize), 57 | } 58 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod command; 2 | mod data; 3 | mod view; 4 | 5 | use command::State; 6 | use data::{Recorded, Select, Text}; 7 | use std::env; 8 | use std::io::stdin; 9 | use syntect::dumps::from_binary; 10 | use syntect::highlighting::Theme; 11 | use syntect::parsing::SyntaxSet; 12 | use termion::input::TermRead; 13 | use view::View; 14 | 15 | fn main() { 16 | let args = env::args(); 17 | 18 | if args.len() > 1 { 19 | for filename in args.skip(1) { 20 | edit_file(&Some(filename)); 21 | } 22 | } else { 23 | edit_file(&None); 24 | } 25 | } 26 | 27 | fn edit_file(filename: &Option) { 28 | let ps = SyntaxSet::load_defaults_nonewlines(); 29 | let ts: Theme = from_binary(include_bytes!("../assets/gruvbox.themedump")); 30 | let mut text = build_text(&filename); 31 | let mut view = build_view(&filename, &ps, &ts); 32 | let mut state = State::Insert; 33 | 34 | let stdin = stdin(); 35 | 36 | view.render(&text); 37 | 38 | let mut events = stdin.events(); 39 | loop { 40 | if let Some(event) = events.next() { 41 | state = match state.handle(&mut text, &mut view, event.unwrap()) { 42 | State::Exit => break, 43 | State::Open(new_filename) => { 44 | // we must close the terminal modes before resetting them 45 | drop(text); 46 | drop(view); 47 | text = build_text(&Some(new_filename.clone())); 48 | view = build_view(&Some(new_filename.clone()), &ps, &ts); 49 | view.message(&format!("Opened {}", new_filename)); 50 | State::Insert 51 | } 52 | state => state, 53 | } 54 | } 55 | 56 | view.render(&text); 57 | } 58 | } 59 | 60 | fn build_text(filename: &Option) -> Select> { 61 | Select::new(Recorded::new(match filename { 62 | Some(name) => match Text::open_file(name.clone()) { 63 | Ok(v) => v, 64 | Err(e) => panic!("{}", e), 65 | }, 66 | None => Text::empty(), 67 | })) 68 | } 69 | 70 | fn build_view<'a>(filename: &Option, ps: &'a SyntaxSet, theme: &'a Theme) -> View<'a> { 71 | let syntax = match filename { 72 | Some(filename) => match ps.find_syntax_for_file(filename) { 73 | Ok(Some(syn)) => syn, 74 | _ => ps.find_syntax_plain_text(), 75 | }, 76 | None => ps.find_syntax_plain_text(), 77 | }; 78 | 79 | View::new(theme, syntax, ps) 80 | } 81 | -------------------------------------------------------------------------------- /src/data/select/mod.rs: -------------------------------------------------------------------------------- 1 | use super::{CharIter, Editable, LineIter, Modifiable, Movement, Named, Saveable, Undoable}; 2 | use delegate_attr::delegate; 3 | use std::io::Result; 4 | 5 | pub type Selection = (usize, usize); 6 | 7 | pub trait Selectable { 8 | fn sel(&self) -> &Option; 9 | fn set_sel(&mut self, selection: Selection); 10 | fn reset_sel(&mut self); 11 | fn in_sel(&self, pos: usize) -> bool { 12 | match *self.sel() { 13 | Some((beg, end)) => pos >= beg && pos <= end, 14 | None => false, 15 | } 16 | } 17 | } 18 | 19 | pub struct Select 20 | where 21 | T: Editable, 22 | { 23 | content: T, 24 | sel: Option, 25 | } 26 | 27 | impl Select 28 | where 29 | T: Editable, 30 | { 31 | pub fn new(content: T) -> Select { 32 | Select { content, sel: None } 33 | } 34 | } 35 | 36 | impl Selectable for Select 37 | where 38 | T: Editable, 39 | { 40 | fn sel(&self) -> &Option { 41 | &self.sel 42 | } 43 | 44 | fn set_sel(&mut self, selection: Selection) { 45 | self.sel = Some(selection); 46 | } 47 | 48 | fn reset_sel(&mut self) { 49 | self.sel = None; 50 | } 51 | } 52 | 53 | #[delegate(self.content)] 54 | impl Editable for Select 55 | where 56 | T: Editable, 57 | { 58 | fn step(&mut self, mov: Movement) -> (); 59 | fn move_to(&mut self, pos: usize) -> (); 60 | fn move_at(&mut self, line: usize, col: usize) -> (); 61 | fn insert(&mut self, c: char) -> (); 62 | fn insert_forward(&mut self, c: char) -> (); 63 | fn delete(&mut self) -> Option; 64 | fn delete_forward(&mut self) -> Option; 65 | fn pos(&self) -> usize; 66 | fn line(&self) -> usize; 67 | fn col(&self) -> usize; 68 | fn line_count(&self) -> usize; 69 | fn len(&self) -> usize; 70 | fn iter(&self) -> CharIter; 71 | fn lines(&self) -> LineIter; 72 | fn iter_line(&self, line: usize) -> CharIter; 73 | fn line_index_to_char_index(&self, line: usize) -> usize; 74 | } 75 | 76 | #[delegate(self.content)] 77 | impl Saveable for Select 78 | where 79 | T: Editable + Saveable, 80 | { 81 | fn save(&mut self) -> Result<()>; 82 | } 83 | 84 | #[delegate(self.content)] 85 | impl Named for Select 86 | where 87 | T: Editable + Named, 88 | { 89 | fn name(&self) -> &String; 90 | fn set_name(&mut self, name: String) -> (); 91 | } 92 | 93 | #[delegate(self.content)] 94 | impl Undoable for Select 95 | where 96 | T: Editable + Undoable, 97 | { 98 | fn undo(&mut self) -> (); 99 | fn redo(&mut self) -> (); 100 | fn history_len(&self) -> usize; 101 | } 102 | 103 | #[delegate(self.content)] 104 | impl Modifiable for Select 105 | where 106 | T: Editable + Modifiable, 107 | { 108 | fn was_modified(&self) -> bool; 109 | } 110 | -------------------------------------------------------------------------------- /src/data/record/action.rs: -------------------------------------------------------------------------------- 1 | use super::super::Editable; 2 | use std::mem; 3 | 4 | #[derive(Clone, Debug)] 5 | pub enum Action { 6 | Insert(String), 7 | InsertForward(String), 8 | Delete(String), 9 | Move(isize), 10 | DeleteForward(String), 11 | } 12 | 13 | impl Action { 14 | pub fn apply(&self, content: &mut T) { 15 | match *self { 16 | Action::Insert(ref s) => { 17 | for c in s.chars() { 18 | content.insert(c); 19 | } 20 | } 21 | Action::Delete(ref s) => { 22 | for _ in s.chars() { 23 | content.delete(); 24 | } 25 | } 26 | Action::Move(rel) => { 27 | let new_position = rel + content.pos() as isize; 28 | content.move_to(new_position as usize); 29 | } 30 | Action::DeleteForward(ref s) => { 31 | for _ in s.chars() { 32 | content.delete_forward(); 33 | } 34 | } 35 | Action::InsertForward(ref s) => { 36 | for c in s.chars() { 37 | content.insert_forward(c); 38 | } 39 | } 40 | }; 41 | } 42 | 43 | pub fn invert(&self) -> Action { 44 | match *self { 45 | Action::Insert(ref s) => Action::Delete(s.clone()), 46 | Action::Delete(ref s) => Action::Insert(s.clone()), 47 | Action::Move(ref rel) => Action::Move(-rel), 48 | Action::InsertForward(ref s) => Action::DeleteForward(s.clone()), 49 | Action::DeleteForward(ref s) => Action::InsertForward(s.clone()), 50 | } 51 | } 52 | 53 | pub fn join(&mut self, act: Action) { 54 | assert!(self.same_variant(&act)); 55 | match *self { 56 | Action::Insert(ref mut s) => { 57 | let act_string = match act { 58 | Action::Insert(a) => a, 59 | _ => panic!("Trying to join dissimilar Actions"), 60 | }; 61 | s.push_str(&act_string); 62 | } 63 | Action::InsertForward(ref mut s) => { 64 | let act_string = match act { 65 | Action::InsertForward(a) => a, 66 | _ => panic!("Trying to join dissimilar Actions"), 67 | }; 68 | s.push_str(&act_string); 69 | } 70 | Action::Delete(ref mut s) => { 71 | let mut act_string = match act { 72 | Action::Delete(a) => a, 73 | _ => panic!("Trying to join dissimilar Actions"), 74 | }; 75 | act_string.push_str(s); 76 | *s = act_string; 77 | } 78 | Action::DeleteForward(ref mut s) => { 79 | let mut act_string = match act { 80 | Action::DeleteForward(a) => a, 81 | _ => panic!("Trying to join dissimilar Actions"), 82 | }; 83 | act_string.push_str(s); 84 | *s = act_string; 85 | } 86 | Action::Move(ref mut rel) => { 87 | let act_rel = match act { 88 | Action::Move(a) => a, 89 | _ => panic!("Trying to join dissimilar Actions"), 90 | }; 91 | *rel += act_rel; 92 | } 93 | } 94 | } 95 | 96 | pub fn same_variant(&self, other: &Action) -> bool { 97 | mem::discriminant(self) == mem::discriminant(other) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/data/record/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(unknown_lints)] 2 | 3 | mod action; 4 | 5 | use self::action::Action; 6 | use super::{CharIter, Editable, LineIter, Modifiable, Movement, Named, Saveable}; 7 | use delegate_attr::delegate; 8 | use std::collections::VecDeque; 9 | use std::io::Result; 10 | use std::usize; 11 | 12 | const HISTORY_SIZE: usize = usize::MAX; 13 | const UNDO_SIZE: usize = usize::MAX; 14 | 15 | pub trait Undoable { 16 | fn undo(&mut self); 17 | fn redo(&mut self); 18 | fn history_len(&self) -> usize; 19 | } 20 | 21 | pub struct Recorded 22 | where 23 | T: Editable, 24 | { 25 | content: T, 26 | history: VecDeque, 27 | undone: VecDeque, 28 | } 29 | 30 | impl Recorded 31 | where 32 | T: Editable, 33 | { 34 | pub fn new(content: T) -> Recorded { 35 | Recorded { 36 | content, 37 | history: VecDeque::new(), 38 | undone: VecDeque::new(), 39 | } 40 | } 41 | fn record(&mut self, act: Action) { 42 | self.undone.clear(); // we are branching to a new sequence of events 43 | if let Some(a) = self.history.front_mut() { 44 | if a.same_variant(&act) { 45 | // join similar actions together 46 | a.join(act); 47 | return; 48 | } 49 | } 50 | self.history.push_front(act); 51 | #[allow(clippy::absurd_extreme_comparisons)] 52 | while self.history.len() > HISTORY_SIZE { 53 | self.history.pop_back(); 54 | } 55 | } 56 | } 57 | 58 | impl Undoable for Recorded 59 | where 60 | T: Editable, 61 | { 62 | fn undo(&mut self) { 63 | let to_undo = match self.history.pop_front() { 64 | None => return, 65 | Some(a) => a, 66 | }; 67 | self.undone.push_front(to_undo.clone()); 68 | #[allow(clippy::absurd_extreme_comparisons)] 69 | while self.undone.len() > UNDO_SIZE { 70 | self.undone.pop_back(); 71 | } 72 | to_undo.invert().apply(&mut self.content); 73 | } 74 | fn redo(&mut self) { 75 | let to_redo = match self.undone.pop_front() { 76 | None => return, 77 | Some(a) => a, 78 | }; 79 | to_redo.apply(&mut self.content); 80 | self.history.push_front(to_redo); 81 | } 82 | fn history_len(&self) -> usize { 83 | self.history.len() 84 | } 85 | } 86 | 87 | impl Editable for Recorded 88 | where 89 | T: Editable, 90 | { 91 | fn step(&mut self, mov: Movement) { 92 | let from = self.content.pos(); 93 | self.content.step(mov); 94 | let to = self.content.pos(); 95 | self.record(Action::Move(to as isize - from as isize)); 96 | } 97 | 98 | fn move_to(&mut self, pos: usize) { 99 | let from = self.content.pos(); 100 | self.content.move_to(pos); 101 | let to = self.content.pos(); 102 | self.record(Action::Move(to as isize - from as isize)); 103 | } 104 | 105 | fn move_at(&mut self, line: usize, col: usize) { 106 | let from = self.content.pos(); 107 | self.content.move_at(line, col); 108 | let to = self.content.pos(); 109 | self.record(Action::Move(to as isize - from as isize)); 110 | } 111 | 112 | fn insert(&mut self, c: char) { 113 | let mut s = String::new(); 114 | s.push(c); 115 | self.record(Action::Insert(s)); 116 | self.content.insert(c); 117 | } 118 | 119 | fn insert_forward(&mut self, c: char) { 120 | let mut s = String::new(); 121 | s.push(c); 122 | self.record(Action::InsertForward(s)); 123 | self.content.insert_forward(c); 124 | } 125 | 126 | fn delete(&mut self) -> Option { 127 | let c = self.content.delete(); 128 | if let Some(c) = c { 129 | let mut s = String::new(); 130 | s.push(c); 131 | self.record(Action::Delete(s)) 132 | } 133 | c 134 | } 135 | 136 | fn delete_forward(&mut self) -> Option { 137 | let c = self.content.delete_forward(); 138 | if let Some(c) = c { 139 | let mut s = String::new(); 140 | s.push(c); 141 | self.record(Action::DeleteForward(s)) 142 | } 143 | c 144 | } 145 | 146 | #[delegate(self.content)] 147 | fn pos(&self) -> usize; 148 | #[delegate(self.content)] 149 | fn line(&self) -> usize; 150 | #[delegate(self.content)] 151 | fn col(&self) -> usize; 152 | #[delegate(self.content)] 153 | fn line_count(&self) -> usize; 154 | #[delegate(self.content)] 155 | fn len(&self) -> usize; 156 | #[delegate(self.content)] 157 | fn iter(&self) -> CharIter; 158 | #[delegate(self.content)] 159 | fn lines(&self) -> LineIter; 160 | #[delegate(self.content)] 161 | fn iter_line(&self, line: usize) -> CharIter; 162 | #[delegate(self.content)] 163 | fn line_index_to_char_index(&self, line: usize) -> usize; 164 | } 165 | 166 | impl Saveable for Recorded 167 | where 168 | T: Editable + Saveable, 169 | { 170 | fn save(&mut self) -> Result<()> { 171 | self.content.save() 172 | } 173 | } 174 | 175 | #[delegate(self.content)] 176 | impl Named for Recorded 177 | where 178 | T: Editable + Named, 179 | { 180 | fn name(&self) -> &String; 181 | fn set_name(&mut self, name: String) -> (); 182 | } 183 | 184 | #[delegate(self.content)] 185 | impl Modifiable for Recorded 186 | where 187 | T: Editable + Modifiable, 188 | { 189 | fn was_modified(&self) -> bool; 190 | } 191 | -------------------------------------------------------------------------------- /src/view/screen.rs: -------------------------------------------------------------------------------- 1 | use ndarray::{Array, Array2}; 2 | use std::cell::RefCell; 3 | use std::fmt::Write as FmtWrite; 4 | use std::io; 5 | use std::io::{BufWriter, Write}; 6 | use syntect::highlighting::Style; 7 | use termion::color; 8 | use termion::input::MouseTerminal; 9 | use termion::raw::{IntoRawMode, RawTerminal}; 10 | use termion::screen::AlternateScreen; 11 | 12 | pub struct Screen { 13 | out: RefCell>>>>, 14 | write_buf: RefCell>, 15 | read_buf: RefCell>, 16 | cursor_pos: (usize, usize), 17 | cursor_visible: bool, 18 | default_style: Style, 19 | } 20 | 21 | impl Screen { 22 | pub fn with_default_style(default_style: Style) -> Self { 23 | let (w, h) = termion::terminal_size().unwrap(); 24 | let write_buf: Array<_, _> = std::iter::repeat((default_style, ' ')) 25 | .take(w as usize * h as usize) 26 | .collect(); 27 | let write_buf = write_buf.into_shape((h as usize, w as usize)).unwrap(); 28 | let read_buf: Array<_, _> = 29 | (std::iter::repeat((default_style, 'X')).take(w as usize * h as usize)).collect(); 30 | let read_buf = read_buf.into_shape((h as usize, w as usize)).unwrap(); 31 | let out = RefCell::new( 32 | MouseTerminal::from(AlternateScreen::from(BufWriter::with_capacity( 33 | 1 << 14, 34 | io::stdout(), 35 | ))) 36 | .into_raw_mode() 37 | .unwrap(), 38 | ); 39 | Screen { 40 | out, 41 | read_buf: RefCell::new(read_buf), 42 | write_buf: RefCell::new(write_buf), 43 | cursor_pos: (0, 0), 44 | cursor_visible: true, 45 | default_style, 46 | } 47 | } 48 | 49 | pub fn clear(&self) { 50 | for cell in self.write_buf.borrow_mut().iter_mut() { 51 | *cell = (self.default_style, ' '); 52 | } 53 | } 54 | 55 | pub fn present(&self) { 56 | let mut out = self.out.borrow_mut(); 57 | let mut read_buf = self.read_buf.borrow_mut(); 58 | let write_buf = self.write_buf.borrow(); 59 | 60 | write!(out, "{}", termion::cursor::Hide).unwrap(); 61 | let mut last_style = self.default_style; 62 | write!(out, "{}", Self::escape_style(&last_style)).unwrap(); 63 | 64 | let (h, w) = write_buf.dim(); 65 | for y in 0..h { 66 | for x in 0..w { 67 | if write_buf[[y, x]] != read_buf[[y, x]] { 68 | read_buf[[y, x]] = write_buf[[y, x]]; 69 | 70 | let (style, ref text) = write_buf[[y, x]]; 71 | if style != last_style { 72 | write!(out, "{}", Self::escape_style(&style)).unwrap(); 73 | last_style = style; 74 | } 75 | write!( 76 | out, 77 | "{}{}", 78 | termion::cursor::Goto(1 + x as u16, 1 + y as u16), 79 | text 80 | ) 81 | .unwrap(); 82 | } 83 | } 84 | } 85 | 86 | if self.cursor_visible { 87 | let (cx, cy) = self.cursor_pos; 88 | write!( 89 | out, 90 | "{}{}", 91 | termion::cursor::Goto(1 + cx as u16, 1 + cy as u16), 92 | termion::cursor::Show, 93 | ) 94 | .unwrap(); 95 | } 96 | 97 | // Make sure everything is written out 98 | out.flush().unwrap(); 99 | } 100 | pub fn draw(&self, x: usize, y: usize, text: &str) { 101 | self.draw_with_style(x, y, self.default_style, text); 102 | } 103 | 104 | pub fn draw_with_style(&self, x: usize, y: usize, style: Style, text: &str) { 105 | self.draw_ranges(x, y, vec![(style, text)]); 106 | } 107 | 108 | pub fn draw_ranges(&self, x: usize, y: usize, ranges: Vec<(Style, &str)>) { 109 | let mut write_buf = self.write_buf.borrow_mut(); 110 | let (h, w) = write_buf.dim(); 111 | if y >= h { 112 | return; 113 | } 114 | let mut x = x; 115 | for (style, text) in ranges { 116 | for g in text.chars() { 117 | if x >= w { 118 | break; 119 | } 120 | write_buf[[y, x]] = (style, g); 121 | x += 1; 122 | } 123 | } 124 | } 125 | 126 | pub fn hide_cursor(&mut self) { 127 | self.cursor_visible = false; 128 | } 129 | 130 | pub fn show_cursor(&mut self) { 131 | self.cursor_visible = true; 132 | } 133 | 134 | pub fn move_cursor(&mut self, x: usize, y: usize) { 135 | self.cursor_pos = (x, y); 136 | } 137 | 138 | fn escape_style(style: &Style) -> String { 139 | let mut s = String::new(); 140 | write!( 141 | s, 142 | "\x1b[48;2;{};{};{}m", 143 | style.background.r, style.background.g, style.background.b 144 | ) 145 | .unwrap(); 146 | write!( 147 | s, 148 | "\x1b[38;2;{};{};{}m", 149 | style.foreground.r, style.foreground.g, style.foreground.b 150 | ) 151 | .unwrap(); 152 | s 153 | } 154 | } 155 | 156 | impl Drop for Screen { 157 | fn drop(&mut self) { 158 | write!( 159 | self.out.borrow_mut(), 160 | "{}{}{}", 161 | color::Fg(color::Reset), 162 | color::Bg(color::Reset), 163 | termion::clear::All, 164 | ) 165 | .unwrap(); 166 | self.show_cursor(); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/data/text.rs: -------------------------------------------------------------------------------- 1 | use super::{CharIter, Editable, LineIter, Modifiable, Movement, Named, Saveable}; 2 | use ropey::Rope; 3 | use std::cmp; 4 | use std::fs::File; 5 | use std::io::{BufReader, Error, ErrorKind, Result, Write}; 6 | use std::path::Path; 7 | 8 | #[derive(Debug)] 9 | pub struct Text { 10 | pos: usize, 11 | text: Rope, 12 | name: String, 13 | modified: bool, 14 | } 15 | 16 | impl Text { 17 | pub fn empty() -> Text { 18 | Text { 19 | pos: 0, 20 | text: Rope::from_str("\n"), 21 | name: String::new(), 22 | modified: false, 23 | } 24 | } 25 | pub fn open_file(filename: String) -> Result { 26 | if Path::new(&filename).exists() { 27 | let mut text = Rope::from_reader(BufReader::new(File::open(&filename)?))?; 28 | 29 | let len = text.len_chars(); 30 | if len > 0 { 31 | match text.char(len - 1) { 32 | '\n' => {} 33 | _ => text.insert(len, "\n"), 34 | } 35 | } else { 36 | text.insert(0, "\n"); 37 | } 38 | 39 | Ok(Text { 40 | pos: 0, 41 | text, 42 | name: filename, 43 | modified: false, 44 | }) 45 | } else { 46 | let mut text = Text::empty(); 47 | text.set_name(filename); 48 | text.modified = true; 49 | Ok(text) 50 | } 51 | } 52 | } 53 | 54 | impl Saveable for Text { 55 | fn save(&mut self) -> Result<()> { 56 | if self.name.is_empty() { 57 | return Err(Error::new( 58 | ErrorKind::InvalidInput, 59 | "Can't write file with no name", 60 | )); 61 | } 62 | let mut file = File::create(&self.name)?; 63 | for chunk in self.text.chunks() { 64 | write!(file, "{}", chunk)?; 65 | } 66 | file.sync_all()?; 67 | self.modified = false; 68 | Ok(()) 69 | } 70 | } 71 | 72 | impl Named for Text { 73 | fn name(&self) -> &String { 74 | &self.name 75 | } 76 | fn set_name(&mut self, name: String) { 77 | self.name = name; 78 | } 79 | } 80 | 81 | impl Modifiable for Text { 82 | fn was_modified(&self) -> bool { 83 | self.modified 84 | } 85 | } 86 | 87 | impl Editable for Text { 88 | fn step(&mut self, mov: Movement) { 89 | match mov { 90 | Movement::Up => { 91 | if self.line() > 0 { 92 | let prev_line = self.text.line_to_char(self.line() - 1); 93 | let prev_line_size = 94 | self.text.lines().nth(self.line() - 1).unwrap().len_chars(); 95 | self.pos = prev_line + cmp::min(self.col(), prev_line_size - 1); 96 | } 97 | } 98 | Movement::Down => { 99 | if self.line() < self.line_count() - 1 { 100 | let next_line = self.text.line_to_char(self.line() + 1); 101 | let next_line_size = 102 | self.text.lines().nth(self.line() + 1).unwrap().len_chars(); 103 | self.pos = next_line + cmp::min(self.col(), next_line_size - 1); 104 | } 105 | } 106 | Movement::PageUp(up) => { 107 | let target_line = if self.line() < up { 108 | 0 109 | } else { 110 | self.line() - up 111 | }; 112 | self.pos = self.text.line_to_char(target_line); 113 | } 114 | Movement::PageDown(down) => { 115 | let target_line = if self.line_count() - self.line() < down { 116 | self.line_count() - 1 117 | } else { 118 | self.line() + down 119 | }; 120 | self.pos = self.text.line_to_char(target_line); 121 | } 122 | Movement::Left => { 123 | if self.pos > 0 { 124 | self.pos -= 1; 125 | } 126 | } 127 | Movement::Right => { 128 | if self.pos < self.text.len_chars() - 1 { 129 | self.pos += 1; 130 | } 131 | } 132 | Movement::LineStart => { 133 | let curr_line = self.text.line_to_char(self.line()); 134 | 135 | self.pos = curr_line; 136 | } 137 | Movement::LineEnd => { 138 | let curr_line = self.text.line_to_char(self.line()); 139 | let curr_line_size = self.text.lines().nth(self.line()).unwrap().len_chars(); 140 | self.pos = curr_line + curr_line_size - 1; 141 | } 142 | } 143 | } 144 | 145 | fn insert(&mut self, c: char) { 146 | self.modified = true; 147 | self.text.insert(self.pos, &format!("{}", c)); 148 | self.pos += 1; 149 | } 150 | fn insert_forward(&mut self, c: char) { 151 | self.modified = true; 152 | self.text.insert(self.pos, &format!("{}", c)); 153 | } 154 | 155 | fn delete(&mut self) -> Option { 156 | self.modified = true; 157 | if self.pos == 0 { 158 | None 159 | } else { 160 | self.pos -= 1; 161 | let ch = self.text.char(self.pos); 162 | self.text.remove(self.pos..=self.pos); 163 | Some(ch) 164 | } 165 | } 166 | 167 | fn delete_forward(&mut self) -> Option { 168 | self.modified = true; 169 | if self.pos < self.len() - 1 { 170 | let ch = self.text.char(self.pos); 171 | self.text.remove(self.pos..=self.pos); 172 | Some(ch) 173 | } else { 174 | None 175 | } 176 | } 177 | 178 | fn move_to(&mut self, pos: usize) { 179 | assert!(pos < self.text.len_chars()); 180 | self.pos = pos; 181 | } 182 | 183 | fn move_at(&mut self, line: usize, col: usize) { 184 | let line = cmp::min(line, self.line_count() - 1); 185 | let col = cmp::min(col, self.text.lines().nth(line).unwrap().len_chars() - 1); 186 | self.pos = self.text.line_to_char(line) + col; 187 | } 188 | 189 | fn pos(&self) -> usize { 190 | self.pos 191 | } 192 | 193 | fn line(&self) -> usize { 194 | self.text.char_to_line(self.pos) 195 | } 196 | 197 | fn col(&self) -> usize { 198 | self.pos - self.text.line_to_char(self.line()) 199 | } 200 | 201 | fn line_count(&self) -> usize { 202 | self.text.len_lines() - 1 203 | } 204 | 205 | fn len(&self) -> usize { 206 | self.text.len_chars() 207 | } 208 | 209 | fn iter(&self) -> CharIter { 210 | self.text.chars() 211 | } 212 | 213 | fn lines(&self) -> LineIter { 214 | self.text.lines() 215 | } 216 | 217 | fn iter_line(&self, line: usize) -> CharIter { 218 | self.text.line(line).chars() 219 | } 220 | 221 | fn line_index_to_char_index(&self, line: usize) -> usize { 222 | self.text.line_to_char(line) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/view/mod.rs: -------------------------------------------------------------------------------- 1 | mod screen; 2 | 3 | use self::screen::Screen; 4 | use crate::data::{Editable, Modifiable, Named, Selectable, Undoable}; 5 | use std::{cmp, iter}; 6 | use syntect::easy::HighlightLines; 7 | use syntect::highlighting::Theme; 8 | use syntect::highlighting::{Color, FontStyle, Style}; 9 | use syntect::parsing::{SyntaxReference, SyntaxSet}; 10 | use termion::terminal_size; 11 | pub struct View<'a> { 12 | message: Option, 13 | is_prompt: bool, 14 | line_offset: usize, 15 | screen: Screen, 16 | theme: &'a Theme, 17 | syntax_set: &'a SyntaxSet, 18 | syntax_ref: &'a SyntaxReference, 19 | } 20 | 21 | const TAB_LENGTH: usize = 4; 22 | 23 | impl<'a> View<'a> { 24 | pub fn new( 25 | theme: &'a Theme, 26 | syntax_ref: &'a SyntaxReference, 27 | syntax_set: &'a SyntaxSet, 28 | ) -> Self { 29 | let default_style = Style { 30 | foreground: theme.settings.foreground.unwrap_or(Color::WHITE), 31 | background: theme.settings.background.unwrap_or(Color::BLACK), 32 | font_style: FontStyle::empty(), 33 | }; 34 | View { 35 | message: None, 36 | is_prompt: false, 37 | line_offset: 0, 38 | screen: Screen::with_default_style(default_style), 39 | theme, 40 | syntax_set, 41 | syntax_ref, 42 | } 43 | } 44 | 45 | pub fn message(&mut self, message: &str) { 46 | self.is_prompt = false; 47 | self.message = Some(String::from(message)); 48 | } 49 | 50 | pub fn prompt(&mut self, prompt: &str, message: &str) { 51 | self.is_prompt = true; 52 | let msg = String::from(prompt) + message; 53 | self.message = Some(msg); 54 | } 55 | 56 | pub fn quiet(&mut self) { 57 | self.is_prompt = false; 58 | self.message = None; 59 | } 60 | 61 | pub fn center_view(&mut self, line: usize) { 62 | self.line_offset = line.saturating_sub(self.lines_height() as usize / 2); 63 | } 64 | 65 | pub fn adjust_view(&mut self, line: usize) { 66 | if line < self.line_offset { 67 | self.line_offset = line; 68 | } else if line + 1 >= self.line_offset + self.lines_height() { 69 | self.line_offset = 1 + line - self.lines_height(); 70 | } 71 | } 72 | 73 | pub fn scroll_view(&mut self, offset: isize, content: &T) { 74 | self.line_offset = cmp::min( 75 | cmp::max((self.line_offset as isize) + offset, 0), 76 | (content.line_count() as isize) - 1, 77 | ) as usize; 78 | } 79 | 80 | pub fn render(&mut self, content: &T) 81 | where 82 | T: Editable + Named + Selectable + Undoable + Modifiable, 83 | { 84 | self.screen.clear(); 85 | self.paint_lines(content); 86 | self.paint_status(content); 87 | self.paint_message(); 88 | self.paint_cursor(content); 89 | self.screen.present(); 90 | } 91 | 92 | pub fn translate_coordinates(&self, content: &T, x: u16, y: u16) -> (usize, usize) 93 | where 94 | T: Editable, 95 | { 96 | let line = cmp::min( 97 | (y as isize + self.line_offset as isize - 1) as usize, 98 | content.line_count() - 1, 99 | ); 100 | let visual_col = (cmp::max( 101 | 0, 102 | x as isize - self.line_number_width(content.line_count()) as isize - 2, 103 | )) as usize; 104 | // find out if we clicked through a tab 105 | let col = content 106 | .iter_line(line) 107 | .scan(0, |state, x| { 108 | *state += if x == '\t' { TAB_LENGTH } else { 1 }; 109 | Some(*state) 110 | }) 111 | .take_while(|&x| x <= visual_col) 112 | .count(); 113 | (line, col) 114 | } 115 | 116 | fn paint_message(&self) { 117 | if let Some(ref message) = self.message { 118 | let y = self.lines_height() + 1; 119 | self.screen.draw(0, y, message); 120 | } 121 | } 122 | 123 | fn paint_cursor(&mut self, content: &T) 124 | where 125 | T: Editable + Selectable, 126 | { 127 | // FIXME: don't print the cursor if off screen, though we should in the future for long 128 | // lines 129 | if (content.line()) < self.line_offset 130 | || content.line() >= self.line_offset + self.lines_height() 131 | || content.col() >= self.lines_width(content.line_count()) 132 | || content.sel().is_some() 133 | { 134 | self.screen.hide_cursor(); 135 | return; 136 | } 137 | 138 | // in the case of a prompt, the cursor should be drawn in the message line 139 | let (x, y) = if self.is_prompt { 140 | ( 141 | self.message.clone().unwrap().chars().count(), 142 | self.lines_height() + 1, 143 | ) 144 | } else { 145 | let (a, b) = self.cursor_pos(content); 146 | (a, b) 147 | }; 148 | self.screen.move_cursor(x, y); 149 | self.screen.show_cursor(); 150 | } 151 | 152 | fn paint_status(&self, content: &T) 153 | where 154 | T: Editable + Named + Undoable + Modifiable, 155 | { 156 | let line = content.line(); 157 | let column = content.col(); 158 | let line_count = content.line_count(); 159 | let advance = ((line + 1) as f64 / line_count as f64 * 100.0).floor(); 160 | 161 | let (screen_width, _) = terminal_size().unwrap(); 162 | let empty_line = (0..screen_width).map(|_| ' ').collect::(); 163 | let y = self.lines_height(); 164 | 165 | let style = Style { 166 | background: self.theme.settings.background.unwrap_or(Color::BLACK), 167 | foreground: self.theme.settings.foreground.unwrap_or(Color::WHITE), 168 | font_style: FontStyle::empty(), 169 | }; 170 | 171 | self.screen.draw_with_style(0, y, style, &empty_line); 172 | let mut filename = content.name().clone(); 173 | if content.was_modified() { 174 | filename.push_str(" *"); 175 | } 176 | self.screen.draw_with_style(0, y, style, &filename); 177 | 178 | let position_info = format!("{}% {}/{}: {}", advance, line + 1, line_count, column); 179 | let x = screen_width as usize - position_info.len(); 180 | self.screen.draw_with_style(x, y, style, &position_info); 181 | } 182 | 183 | fn paint_lines(&mut self, content: &T) 184 | where 185 | T: Editable + Selectable, 186 | { 187 | let line_offset = self.line_offset as usize; 188 | let lines_height = self.lines_height() as usize; 189 | let line_count = content.line_count(); 190 | 191 | let line_start = self.line_number_width(line_count) as usize + 1; 192 | 193 | let mut highlighter = HighlightLines::new(self.syntax_ref, self.theme); 194 | 195 | for (i, line) in content.lines().enumerate() { 196 | let line_str = line 197 | .chars() 198 | .flat_map(|c| { 199 | if c == '\t' { 200 | iter::repeat(' ').take(TAB_LENGTH) // FIXME: selection should consider tabs 201 | } else if c == '\n' { 202 | iter::repeat(' ').take(1) 203 | } else { 204 | iter::repeat(c).take(1) 205 | } 206 | }) 207 | .collect::(); 208 | 209 | let ranges: Vec<(Style, &str)> = highlighter.highlight(&line_str, self.syntax_set); 210 | 211 | if i < line_offset { 212 | continue; 213 | } 214 | let y = i - line_offset; 215 | if y >= cmp::min(lines_height, line_count) { 216 | break; 217 | } 218 | 219 | // paint line number and initialize display for this line 220 | let line_index = line_offset + y; 221 | let line_number_style = Style { 222 | background: self.theme.settings.gutter.unwrap_or(Color { 223 | r: 40, 224 | g: 40, 225 | b: 40, 226 | a: 255, 227 | }), 228 | foreground: self.theme.settings.gutter_foreground.unwrap_or(Color { 229 | r: 146, 230 | g: 131, 231 | b: 116, 232 | a: 255, 233 | }), 234 | font_style: FontStyle::empty(), 235 | }; 236 | self.screen 237 | .draw_with_style(0, y, line_number_style, &format!("{}", 1 + line_index)); 238 | 239 | self.screen.draw_ranges(line_start, y, ranges); 240 | 241 | // draw selection over 242 | if let Some((selbeg, selend)) = content.sel() { 243 | let selection_style = Style { 244 | foreground: self 245 | .theme 246 | .settings 247 | .selection_foreground 248 | .unwrap_or(Color::WHITE), 249 | background: self.theme.settings.selection.unwrap_or(Color::BLACK), 250 | font_style: FontStyle::empty(), 251 | }; 252 | let beg = content.line_index_to_char_index(line_index); 253 | let end = beg + line_str.len() - 1; 254 | 255 | if *selbeg <= beg && *selend >= end { 256 | // line is fully inside the selection 257 | if line_str.is_empty() { 258 | self.screen 259 | .draw_with_style(line_start, y, selection_style, " "); 260 | } else { 261 | self.screen 262 | .draw_with_style(line_start, y, selection_style, &line_str); 263 | } 264 | } else if *selbeg >= beg && *selbeg <= end && *selend <= end && *selend >= beg { 265 | // selection is inside the line 266 | self.screen.draw_with_style( 267 | line_start + (*selbeg - beg), 268 | y, 269 | selection_style, 270 | &line_str[(*selbeg - beg)..=(*selend - beg)], 271 | ); 272 | } else if *selbeg <= end && *selbeg >= beg { 273 | // line contains the beginning of the selection 274 | self.screen.draw_with_style( 275 | line_start + (*selbeg - beg), 276 | y, 277 | selection_style, 278 | &line_str[(*selbeg - beg)..], 279 | ); 280 | } else if *selend <= end && *selend >= beg { 281 | // line contains the end of the selection 282 | self.screen.draw_with_style( 283 | line_start, 284 | y, 285 | selection_style, 286 | &line_str[..=(*selend - beg)], 287 | ); 288 | } 289 | } 290 | } 291 | } 292 | 293 | fn cursor_pos(&self, content: &T) -> (usize, usize) { 294 | // TODO: column offsetting for long lines 295 | let line = content.line(); 296 | let first_line = self.line_offset; 297 | let y = line - first_line as usize; 298 | // we can't trust the actual column because tabs have variable length 299 | let visual_col = content.col(); 300 | let column: usize = content 301 | .iter_line(line) 302 | .map(|x| if x == '\t' { TAB_LENGTH } else { 1 }) 303 | .take(visual_col) 304 | .sum(); 305 | ( 306 | (self.line_number_width(content.line_count()) as usize + 1 + column), 307 | y, 308 | ) 309 | } 310 | 311 | fn line_number_width(&self, line_count: usize) -> u16 { 312 | line_count.to_string().len() as u16 313 | } 314 | 315 | fn status_height(&self) -> u16 { 316 | 2 317 | } 318 | 319 | pub fn lines_height(&self) -> usize { 320 | let (_, screen_height) = terminal_size().unwrap(); 321 | let incompressible = self.status_height() as usize; 322 | cmp::max(screen_height as usize, incompressible) - incompressible 323 | } 324 | 325 | pub fn lines_width(&self, line_count: usize) -> usize { 326 | let (screen_width, _) = terminal_size().unwrap(); 327 | let incompressible = self.line_number_width(line_count) as usize + 1; 328 | cmp::max(screen_width as usize, incompressible) - incompressible 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/command/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::data::*; 2 | use crate::view::View; 3 | use clipboard::{ClipboardContext, ClipboardProvider}; 4 | use std::cmp; 5 | use termion::event::{Event, Key, MouseButton, MouseEvent}; 6 | 7 | #[derive(Debug, Clone)] 8 | pub enum State { 9 | Insert, 10 | Message, 11 | Prompt(String, String, PromptAction), 12 | Select(usize), 13 | Selected, 14 | Open(String), 15 | Exit, 16 | } 17 | 18 | #[derive(Debug, Clone)] 19 | pub enum PromptAction { 20 | Save, 21 | ConfirmExit, 22 | Open, 23 | ConfirmOpen(String), 24 | } 25 | 26 | const SCROLL_FACTOR: usize = 2; 27 | 28 | impl State { 29 | // Handles a Termion event, consuming the current state and returning the new state 30 | pub fn handle(self, content: &mut T, view: &mut View, event: Event) -> Self 31 | where 32 | T: Editable + Saveable + Undoable + Selectable + Modifiable, 33 | { 34 | match self { 35 | State::Prompt(prompt, message, action) => { 36 | State::handle_prompt(content, view, event, prompt, message, action) 37 | } 38 | State::Select(origin) => State::handle_select(content, view, event, origin), 39 | State::Insert => State::handle_insert(content, view, event), 40 | State::Message => State::handle_message(content, view, event), 41 | State::Selected => State::handle_selected(content, view, event), 42 | State::Open(_) | State::Exit => panic!("Can't handle exit state"), 43 | } 44 | } 45 | 46 | fn handle_message(content: &mut T, view: &mut View, event: Event) -> Self 47 | where 48 | T: Editable + Named + Undoable + Modifiable + Saveable, 49 | { 50 | view.quiet(); 51 | Self::handle_insert(content, view, event) 52 | } 53 | 54 | fn handle_insert(content: &mut T, view: &mut View, event: Event) -> Self 55 | where 56 | T: Editable + Named + Undoable + Modifiable + Saveable, 57 | { 58 | match event { 59 | Event::Key(Key::Ctrl('q')) | Event::Key(Key::Esc) => { 60 | if content.was_modified() { 61 | let prompt = "Changes not saved do you really want to exit (y/N): ".to_string(); 62 | let message = "".to_string(); 63 | view.prompt(&prompt, &message); 64 | return State::Prompt(prompt, message, PromptAction::ConfirmExit); 65 | } else { 66 | return State::Exit; 67 | } 68 | } 69 | Event::Key(Key::Ctrl('s')) => { 70 | if content.name().is_empty() { 71 | let prompt = "Save to: ".to_string(); 72 | view.prompt(&prompt, ""); 73 | return State::Prompt(prompt, "".to_string(), PromptAction::Save); 74 | } else { 75 | let msg = match content.save() { 76 | Err(e) => e.to_string(), 77 | Ok(_) => format!("Saved file {}", content.name()), 78 | }; 79 | view.message(&msg); 80 | return State::Message; 81 | } 82 | } 83 | Event::Key(Key::Ctrl('o')) => { 84 | let prompt = "Open file: ".to_string(); 85 | let message = "".to_string(); 86 | view.prompt(&prompt, &message); 87 | return State::Prompt(prompt, message, PromptAction::Open); 88 | } 89 | Event::Mouse(MouseEvent::Press(MouseButton::Left, x, y)) => { 90 | let (line, col) = view.translate_coordinates(content, x, y); 91 | content.move_at(line, col); 92 | return State::Select(content.pos()); 93 | } 94 | Event::Mouse(MouseEvent::Press(MouseButton::WheelDown, _, _)) => { 95 | view.scroll_view(SCROLL_FACTOR as isize, content); 96 | } 97 | Event::Mouse(MouseEvent::Press(MouseButton::WheelUp, _, _)) => { 98 | view.scroll_view(-(SCROLL_FACTOR as isize), content); 99 | } 100 | Event::Key(Key::Ctrl('z')) => { 101 | content.undo(); 102 | } 103 | Event::Key(Key::Ctrl('y')) => { 104 | content.redo(); 105 | } 106 | Event::Key(Key::Ctrl('v')) => { 107 | let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); 108 | for c in ctx 109 | .get_contents() 110 | .unwrap_or_else(|_| "".to_string()) 111 | .chars() 112 | { 113 | content.insert(c); 114 | } 115 | } 116 | Event::Key(Key::Up) => { 117 | content.step(Movement::Up); 118 | view.adjust_view(content.line()); 119 | } 120 | Event::Key(Key::Down) => { 121 | content.step(Movement::Down); 122 | view.adjust_view(content.line()); 123 | } 124 | Event::Key(Key::Left) => { 125 | content.step(Movement::Left); 126 | view.adjust_view(content.line()); 127 | } 128 | Event::Key(Key::Right) => { 129 | content.step(Movement::Right); 130 | view.adjust_view(content.line()); 131 | } 132 | Event::Key(Key::PageUp) => { 133 | content.step(Movement::PageUp(view.lines_height() as usize)); 134 | view.center_view(content.line()); 135 | } 136 | Event::Key(Key::PageDown) => { 137 | content.step(Movement::PageDown(view.lines_height() as usize)); 138 | view.center_view(content.line()); 139 | } 140 | Event::Key(Key::Home) => { 141 | content.step(Movement::LineStart); 142 | } 143 | Event::Key(Key::End) => { 144 | content.step(Movement::LineEnd); 145 | } 146 | Event::Key(Key::Backspace) | Event::Key(Key::Ctrl('h')) => { 147 | content.delete(); 148 | view.adjust_view(content.line()); 149 | } 150 | Event::Key(Key::Delete) => { 151 | content.delete_forward(); 152 | view.adjust_view(content.line()); 153 | } 154 | Event::Key(Key::Char(c)) => { 155 | content.insert(c); 156 | view.adjust_view(content.line()); 157 | } 158 | Event::Unsupported(u) => { 159 | view.message(&format!("Unsupported escape sequence {:?}", u)); 160 | } 161 | _ => {} 162 | } 163 | State::Insert 164 | } 165 | 166 | fn handle_prompt( 167 | content: &mut T, 168 | view: &mut View, 169 | event: Event, 170 | prompt: String, 171 | mut message: String, 172 | action: PromptAction, 173 | ) -> Self 174 | where 175 | T: Editable + Saveable + Modifiable, 176 | { 177 | match event { 178 | Event::Key(Key::Char('\n')) => match action { 179 | PromptAction::Save => { 180 | let msg: String; 181 | let old_name = content.name().clone(); 182 | content.set_name(message.clone()); 183 | msg = match content.save() { 184 | Err(e) => { 185 | content.set_name(old_name); 186 | e.to_string() 187 | } 188 | Ok(_) => format!("Saved file {}", message), 189 | }; 190 | view.message(&msg); 191 | State::Message 192 | } 193 | PromptAction::ConfirmExit => { 194 | if message.to_lowercase() == "y" { 195 | State::Exit 196 | } else { 197 | view.message(""); 198 | State::Message 199 | } 200 | } 201 | PromptAction::Open => { 202 | let filename = message; 203 | if content.was_modified() { 204 | let prompt = 205 | "Changes not saved do you really want to open a new file (y/N): " 206 | .to_string(); 207 | let message = "".to_string(); 208 | view.prompt(&prompt, &message); 209 | State::Prompt(prompt, message, PromptAction::ConfirmOpen(filename)) 210 | } else { 211 | State::Open(filename) 212 | } 213 | } 214 | PromptAction::ConfirmOpen(filename) => { 215 | if message.to_lowercase() == "y" { 216 | State::Open(filename) 217 | } else { 218 | view.message(""); 219 | State::Message 220 | } 221 | } 222 | }, 223 | Event::Key(Key::Char('\t')) => State::Prompt(prompt, message, action), // TODO: autocompletion 224 | Event::Key(Key::Char(c)) => { 225 | message.push(c); 226 | view.prompt(&prompt, &message); 227 | State::Prompt(prompt, message, action) 228 | } 229 | Event::Key(Key::Backspace) | Event::Key(Key::Delete) => { 230 | message.pop(); 231 | view.prompt(&prompt, &message); 232 | State::Prompt(prompt, message, action) 233 | } 234 | Event::Key(Key::Ctrl('q')) => State::Exit, 235 | Event::Key(Key::Esc) => { 236 | view.quiet(); 237 | State::Insert 238 | } 239 | _ => State::Prompt(prompt, message, action), 240 | } 241 | } 242 | 243 | fn handle_selected(content: &mut T, view: &mut View, event: Event) -> Self 244 | where 245 | T: Selectable + Editable + Named + Undoable + Modifiable + Saveable, 246 | { 247 | match event { 248 | Event::Key(Key::Ctrl('c')) => { 249 | let (beg, end) = content.sel().unwrap(); 250 | 251 | let selection: String = content.iter().skip(beg).take(end - beg + 1).collect(); 252 | let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); 253 | ctx.set_contents(selection).unwrap(); 254 | 255 | content.reset_sel(); 256 | State::Insert 257 | } 258 | Event::Key(Key::Ctrl('x')) => { 259 | let (beg, end) = content.sel().unwrap(); 260 | 261 | let selection: String = content.iter().skip(beg).take(end - beg + 1).collect(); 262 | let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); 263 | ctx.set_contents(selection).unwrap(); 264 | 265 | delete_sel(content); 266 | view.adjust_view(content.line()); 267 | 268 | content.reset_sel(); 269 | State::Insert 270 | } 271 | Event::Key(Key::Backspace) | Event::Key(Key::Delete) => { 272 | delete_sel(content); 273 | view.adjust_view(content.line()); 274 | content.reset_sel(); 275 | State::Insert 276 | } 277 | Event::Key(Key::Char(_)) => { 278 | delete_sel(content); 279 | view.adjust_view(content.line()); 280 | content.reset_sel(); 281 | Self::handle_insert(content, view, event) 282 | } 283 | _ => { 284 | content.reset_sel(); 285 | Self::handle_insert(content, view, event) 286 | } 287 | } 288 | } 289 | 290 | fn handle_select(content: &mut T, view: &mut View, event: Event, origin: usize) -> Self 291 | where 292 | T: Editable + Selectable, 293 | { 294 | match event { 295 | Event::Mouse(MouseEvent::Hold(x, y)) => { 296 | let (line, col) = view.translate_coordinates(content, x, y); 297 | content.move_at(line, col); 298 | let sel = ( 299 | cmp::min(origin, content.pos()), 300 | cmp::max(origin, content.pos()), 301 | ); 302 | content.set_sel(sel); 303 | State::Select(origin) 304 | } 305 | Event::Mouse(MouseEvent::Release(x, y)) => { 306 | let (line, col) = view.translate_coordinates(content, x, y); 307 | content.move_at(line, col); 308 | if origin != content.pos() { 309 | let sel = ( 310 | cmp::min(origin, content.pos()), 311 | cmp::max(origin, content.pos()), 312 | ); 313 | content.set_sel(sel); 314 | State::Selected 315 | } else { 316 | State::Insert 317 | } 318 | } 319 | _ => State::Select(origin), 320 | } 321 | } 322 | } 323 | 324 | fn delete_sel(content: &mut T) 325 | where 326 | T: Selectable + Editable, 327 | { 328 | let (beg, end) = content.sel().unwrap(); 329 | assert!(beg < end); 330 | let end = cmp::min(end + 1, content.len() - 1); 331 | content.move_to(end); 332 | for _ in beg..end { 333 | content.delete(); 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /assets/gruvbox.tmTheme: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | comment 10 | Based on original gruvbox color scheme. 11 | author 12 | peaceant 13 | name 14 | gruvbox 15 | settings 16 | 17 | 18 | settings 19 | 20 | background 21 | #282828 22 | caret 23 | #fcf9e3 24 | foreground 25 | #fdf4c1aa 26 | gutterForeground 27 | #fabd2f 28 | lineHighlight 29 | #3c3836 30 | selection 31 | #504945 32 | bracketContentsForeground 33 | #928374 34 | bracketsForeground 35 | #d5c4a1 36 | guide 37 | #3c3836 38 | activeGuide 39 | #a89984 40 | stackGuide 41 | #665c54 42 | 43 | 44 | 45 | name 46 | Punctuation 47 | scope 48 | punctuation.definition.tag 49 | settings 50 | 51 | fontStyle 52 | 53 | foreground 54 | #83a598 55 | 56 | 57 | 58 | name 59 | Punctuation 60 | scope 61 | punctuation.definition.entity 62 | settings 63 | 64 | fontStyle 65 | 66 | foreground 67 | #d3869b 68 | 69 | 70 | 71 | name 72 | Constant 73 | scope 74 | constant 75 | settings 76 | 77 | fontStyle 78 | 79 | foreground 80 | #d3869b 81 | 82 | 83 | 84 | name 85 | Constant escape 86 | scope 87 | constant.character.escape 88 | settings 89 | 90 | fontStyle 91 | 92 | foreground 93 | #b8bb26 94 | 95 | 96 | 97 | name 98 | Constant other 99 | scope 100 | constant.other 101 | settings 102 | 103 | fontStyle 104 | 105 | foreground 106 | #fdf4c1 107 | 108 | 109 | 110 | name 111 | Entity 112 | scope 113 | entity 114 | settings 115 | 116 | fontStyle 117 | 118 | foreground 119 | #8ec07c 120 | 121 | 122 | 123 | name 124 | Keyword 125 | scope 126 | keyword.operator.comparison, keyword.operator, keyword.operator.symbolic, keyword.operator.string, keyword.operator.assignment, keyword.operator.arithmetic, keyword.operator.class, keyword.operator.key, keyword.operator.logical 127 | settings 128 | 129 | fontStyle 130 | 131 | foreground 132 | #fe8019 133 | 134 | 135 | 136 | name 137 | Keyword 138 | scope 139 | keyword, keyword.operator.new, keyword.other, keyword.control 140 | settings 141 | 142 | fontStyle 143 | 144 | foreground 145 | #fa5c4b 146 | 147 | 148 | 149 | name 150 | Storage 151 | scope 152 | storage 153 | settings 154 | 155 | fontStyle 156 | 157 | foreground 158 | #fa5c4b 159 | 160 | 161 | 162 | name 163 | String 164 | scope 165 | string -string.unquoted.old-plist -string.unquoted.heredoc, string.unquoted.heredoc string 166 | settings 167 | 168 | fontStyle 169 | 170 | foreground 171 | #b8bb26 172 | 173 | 174 | 175 | name 176 | Comment 177 | scope 178 | comment 179 | settings 180 | 181 | fontStyle 182 | italic 183 | foreground 184 | #928374 185 | 186 | 187 | 188 | name 189 | Regexp 190 | scope 191 | string.regexp constant.character.escape 192 | settings 193 | 194 | foreground 195 | #b8bb26 196 | 197 | 198 | 199 | name 200 | Support 201 | scope 202 | support 203 | settings 204 | 205 | fontStyle 206 | 207 | foreground 208 | #fabd2f 209 | 210 | 211 | 212 | name 213 | Variable 214 | scope 215 | variable 216 | settings 217 | 218 | fontStyle 219 | 220 | foreground 221 | #fdf4c1 222 | 223 | 224 | 225 | name 226 | Lang Variable 227 | scope 228 | variable.language 229 | settings 230 | 231 | fontStyle 232 | 233 | foreground 234 | #fdf4c1 235 | 236 | 237 | 238 | name 239 | Function Call 240 | scope 241 | meta.function-call 242 | settings 243 | 244 | foreground 245 | #fdf4c1 246 | 247 | 248 | 249 | name 250 | Invalid 251 | scope 252 | invalid 253 | settings 254 | 255 | background 256 | #932b1e 257 | foreground 258 | #fdf4c1 259 | 260 | 261 | 262 | name 263 | Embedded Source 264 | scope 265 | text source, string.unquoted.heredoc, source source 266 | settings 267 | 268 | fontStyle 269 | 270 | foreground 271 | #fdf4c1 272 | 273 | 274 | 275 | name 276 | String embedded-source 277 | scope 278 | string.quoted source 279 | settings 280 | 281 | fontStyle 282 | 283 | foreground 284 | #b8bb26 285 | 286 | 287 | 288 | name 289 | String constant 290 | scope 291 | string 292 | settings 293 | 294 | foreground 295 | #b8bb26 296 | 297 | 298 | 299 | name 300 | Support.constant 301 | scope 302 | support.constant 303 | settings 304 | 305 | fontStyle 306 | 307 | foreground 308 | #fabd2f 309 | 310 | 311 | 312 | name 313 | Support.class 314 | scope 315 | support.class 316 | settings 317 | 318 | fontStyle 319 | 320 | foreground 321 | #8ec07c 322 | 323 | 324 | 325 | name 326 | Meta.tag.A 327 | scope 328 | entity.name.tag 329 | settings 330 | 331 | fontStyle 332 | bold 333 | foreground 334 | #8ec07c 335 | 336 | 337 | 338 | name 339 | Inner tag 340 | scope 341 | meta.tag, meta.tag entity 342 | settings 343 | 344 | foreground 345 | #8ec07c 346 | 347 | 348 | 349 | name 350 | css colors 351 | scope 352 | constant.other.color.rgb-value 353 | settings 354 | 355 | foreground 356 | #83a598 357 | 358 | 359 | 360 | name 361 | css tag-name 362 | scope 363 | meta.selector.css entity.name.tag 364 | settings 365 | 366 | foreground 367 | #fa5c4b 368 | 369 | 370 | 371 | name 372 | css#id 373 | scope 374 | meta.selector.css, entity.other.attribute-name.id 375 | settings 376 | 377 | foreground 378 | #b8bb26 379 | 380 | 381 | 382 | name 383 | css.class 384 | scope 385 | meta.selector.css entity.other.attribute-name.class 386 | settings 387 | 388 | foreground 389 | #b8bb26 390 | 391 | 392 | 393 | name 394 | css property-name: 395 | scope 396 | support.type.property-name.css 397 | settings 398 | 399 | foreground 400 | #8ec07c 401 | 402 | 403 | 404 | name 405 | css @at-rule 406 | scope 407 | meta.preprocessor.at-rule keyword.control.at-rule 408 | settings 409 | 410 | foreground 411 | #fabd2f 412 | 413 | 414 | 415 | name 416 | css additional-constants 417 | scope 418 | meta.property-value constant 419 | settings 420 | 421 | foreground 422 | #fabd2f 423 | 424 | 425 | 426 | name 427 | css additional-constants 428 | scope 429 | meta.property-value support.constant.named-color.css 430 | settings 431 | 432 | foreground 433 | #fe8019 434 | 435 | 436 | 437 | name 438 | css constructor.argument 439 | scope 440 | meta.constructor.argument.css 441 | settings 442 | 443 | foreground 444 | #fabd2f 445 | 446 | 447 | 448 | name 449 | diff.header 450 | scope 451 | meta.diff, meta.diff.header 452 | settings 453 | 454 | foreground 455 | #83a598 456 | 457 | 458 | 459 | name 460 | diff.deleted 461 | scope 462 | markup.deleted 463 | settings 464 | 465 | foreground 466 | #fa5c4b 467 | 468 | 469 | 470 | name 471 | diff.changed 472 | scope 473 | markup.changed 474 | settings 475 | 476 | foreground 477 | #fabd2f 478 | 479 | 480 | 481 | name 482 | diff.inserted 483 | scope 484 | markup.inserted 485 | settings 486 | 487 | foreground 488 | #8ec07c 489 | 490 | 491 | 492 | name 493 | Bold Markup 494 | scope 495 | markup.bold 496 | settings 497 | 498 | fontStyle 499 | bold 500 | 501 | 502 | 503 | name 504 | Italic Markup 505 | scope 506 | markup.italic 507 | settings 508 | 509 | fontStyle 510 | italic 511 | 512 | 513 | 514 | name 515 | Heading Markup 516 | scope 517 | markup.heading 518 | settings 519 | 520 | fontStyle 521 | bold 522 | foreground 523 | #8ec07c 524 | 525 | 526 | 527 | name 528 | PHP: class name 529 | scope 530 | entity.name.type.class.php 531 | settings 532 | 533 | foreground 534 | #8ec07c 535 | 536 | 537 | 538 | name 539 | PHP: Comment 540 | scope 541 | keyword.other.phpdoc 542 | settings 543 | 544 | fontStyle 545 | 546 | foreground 547 | #928374 548 | 549 | 550 | 551 | name 552 | CSS: numbers 553 | scope 554 | constant.numeric.css, keyword.other.unit.css 555 | settings 556 | 557 | foreground 558 | #d3869b 559 | 560 | 561 | 562 | name 563 | CSS: entity dot, hash, comma, etc. 564 | scope 565 | punctuation.definition.entity.css 566 | settings 567 | 568 | foreground 569 | #b8bb26 570 | 571 | 572 | 573 | name 574 | JS: variable 575 | scope 576 | variable.language.js 577 | settings 578 | 579 | foreground 580 | #fabd2f 581 | 582 | 583 | 584 | name 585 | JS: unquoted labe 586 | scope 587 | string.unquoted.label.js 588 | settings 589 | 590 | foreground 591 | #fdf4c1 592 | 593 | 594 | 595 | name 596 | Constant other sql 597 | scope 598 | constant.other.table-name.sql 599 | settings 600 | 601 | fontStyle 602 | 603 | foreground 604 | #b8bb26 605 | 606 | 607 | 608 | name 609 | Constant other sql 610 | scope 611 | constant.other.database-name.sql 612 | settings 613 | 614 | fontStyle 615 | 616 | foreground 617 | #b8bb26 618 | 619 | 620 | 621 | name 622 | dired directory 623 | scope 624 | storage.type.dired.item.directory, dired.item.directory 625 | settings 626 | 627 | foreground 628 | #8ec07c 629 | 630 | 631 | 632 | name 633 | orgmode link 634 | scope 635 | orgmode.link 636 | settings 637 | 638 | foreground 639 | #fabd2f 640 | fontStyle 641 | underline 642 | 643 | 644 | 645 | name 646 | orgmode page 647 | scope 648 | orgmode.page 649 | settings 650 | 651 | foreground 652 | #b8bb26 653 | 654 | 655 | 656 | name 657 | orgmode break 658 | scope 659 | orgmode.break 660 | settings 661 | 662 | foreground 663 | #d3869b 664 | 665 | 666 | 667 | name 668 | orgmode headline 669 | scope 670 | orgmode.headline 671 | settings 672 | 673 | foreground 674 | #8ec07c 675 | 676 | 677 | 678 | name 679 | orgmode tack 680 | scope 681 | orgmode.tack 682 | settings 683 | 684 | foreground 685 | #fabd2f 686 | 687 | 688 | 689 | name 690 | orgmode follow up 691 | scope 692 | orgmode.follow_up 693 | settings 694 | 695 | foreground 696 | #fabd2f 697 | 698 | 699 | 700 | name 701 | orgmode checkbox 702 | scope 703 | orgmode.checkbox 704 | settings 705 | 706 | foreground 707 | #fabd2f 708 | 709 | 710 | 711 | name 712 | orgmode checkbox summary 713 | scope 714 | orgmode.checkbox.summary 715 | settings 716 | 717 | foreground 718 | #fabd2f 719 | 720 | 721 | 722 | name 723 | orgmode tags 724 | scope 725 | orgmode.tags 726 | settings 727 | 728 | foreground 729 | #fa5c4b 730 | 731 | 732 | 733 | uuid 734 | 06CD1FB2-A00A-4F8C-97B2-60E131980454 735 | colorSpaceName 736 | sRGB 737 | semanticClass 738 | theme.dark.gruvbox 739 | 740 | 741 | -------------------------------------------------------------------------------- /img/smith.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml --------------------------------------------------------------------------------