├── Cargo.toml ├── README.md └── src ├── keys.rs └── main.rs /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nwin" 3 | version = "0.1.0" 4 | authors = ["glacambre "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | neovim-lib = { git = "https://github.com/glacambre/neovim-lib" } 11 | swayipc = { git = "https://github.com/glacambre/swayipc-rs" } 12 | home = "0.5.3" 13 | 14 | [dependencies.sdl2] 15 | version = "0.35.1" 16 | features = ["ttf", "unsafe_textures"] 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nwin 2 | 3 | This is an experimental Neovim UI that creates a new OS window for each Neovim window, all backed by the same neovim server. This enables performing window management using i3/sway's regular window-management features. 4 | 5 | ## Why use this 6 | 7 | If you don't care about controlling neovim windows from the comfort of Sway, there is absolutely no reason. [Neovim-qt](https://github.com/equalsraf/neovim-qt), [Goneovim](https://github.com/akiyosi/goneovim) and [Neovide](https://github.com/Kethku/neovide) are all much more polished. 8 | 9 | ## Dependencies 10 | 11 | - The ext-win branch of my [neovim fork](https://github.com/glacambre/neovim/tree/ext-win) (make sure it's compiled and first in your path!). 12 | - SDL and its ttf library (`sudo apt install libsdl2 libsdl2-ttf`) 13 | - The font [NotoSansMono-Regular.ttf](https://noto-website-2.storage.googleapis.com/pkgs/NotoSansMono-hinted.zip ) in `$HOME/downloads/NotoSansMono` (yes, really). 14 | - A very strong stomach if you're going to look at the code. 15 | 16 | ## Obligatory GIF 17 | 18 | ![video](https://user-images.githubusercontent.com/11534587/110248224-4f64c180-7f70-11eb-8ed7-31b930519cff.gif). 19 | -------------------------------------------------------------------------------- /src/keys.rs: -------------------------------------------------------------------------------- 1 | extern crate sdl2; 2 | use sdl2::event::Event; 3 | use sdl2::keyboard::Keycode; 4 | use sdl2::keyboard::Mod; 5 | 6 | // Issues: 7 | // 1) `/` is actually `:`+`Shift`. We need to catch this as returning `` would result in `:/` 8 | // being inserted, which is wrong. 9 | // 2) `>` is actually `<`+`Shift`, where `<` has a non-literal rep. 10 | // 3) ` ` has a non-literal repr, but also a textinput event. Solution: ignore textinput event, as 11 | // it can't handle stuff like while keydown can. ???: Are there other keys in a 12 | // similar situation? 13 | // 4) RALT is ALTGR - frequently used to produce alternative characters. Results in things like 14 | // λ being inserted for a single . Solution: use config to decide whether ralt 15 | // should be listened to? Ignore ralt for now. 16 | fn with_mod(s: &str, m: Mod) -> Option { 17 | let has_gui = (m & Mod::LGUIMOD != Mod::NOMOD) || (m & Mod::RGUIMOD != Mod::NOMOD); 18 | let has_ctrl = (m & Mod::LCTRLMOD != Mod::NOMOD) || (m & Mod::RCTRLMOD != Mod::NOMOD); 19 | let has_alt = m & Mod::LALTMOD != Mod::NOMOD/* || (m & Mod::RALTMOD != Mod::NOMOD)*/; 20 | let has_non_shift_mod = has_gui || has_ctrl || has_alt; 21 | let has_literal_repr = s.chars().next().unwrap() != '<'; 22 | 23 | // Take care of 1) 24 | if has_literal_repr && !has_non_shift_mod { 25 | return None; 26 | } 27 | if s == "" && !has_non_shift_mod { 28 | return None; 29 | } 30 | let mut result = s.to_string(); 31 | if has_literal_repr { 32 | result.insert_str(0, "<"); 33 | result.insert_str(2, ">"); 34 | } 35 | let shifted = (m & Mod::LSHIFTMOD) != Mod::NOMOD || (m & Mod::RSHIFTMOD) != Mod::NOMOD; 36 | if (shifted && !(m & Mod::CAPSMOD != Mod::NOMOD)) 37 | || (!shifted && (m & Mod::CAPSMOD != Mod::NOMOD)) 38 | { 39 | result.insert_str(1, "S-"); 40 | } 41 | if has_gui { 42 | result.insert_str(1, "D-"); 43 | } 44 | if has_ctrl { 45 | result.insert_str(1, "C-"); 46 | } 47 | if has_alt { 48 | result.insert_str(1, "A-"); 49 | } 50 | Some(result) 51 | } 52 | 53 | pub fn nvim_char_representation(c: char) -> Option<&'static str> { 54 | match c { 55 | '<' => Some(""), 56 | ' ' => Some(""), 57 | _ => None, 58 | } 59 | } 60 | 61 | pub fn nvim_event_representation(event: Event) -> Option { 62 | if let Event::KeyDown { 63 | keycode: Some(k), 64 | keymod: m, 65 | .. 66 | } = event 67 | { 68 | match k { 69 | // Alpha 70 | Keycode::A => with_mod("a", m), 71 | Keycode::B => with_mod("b", m), 72 | Keycode::C => with_mod("c", m), 73 | Keycode::D => with_mod("d", m), 74 | Keycode::E => with_mod("e", m), 75 | Keycode::F => with_mod("f", m), 76 | Keycode::G => with_mod("g", m), 77 | Keycode::H => with_mod("h", m), 78 | Keycode::I => with_mod("i", m), 79 | Keycode::J => with_mod("j", m), 80 | Keycode::K => with_mod("k", m), 81 | Keycode::L => with_mod("l", m), 82 | Keycode::M => with_mod("m", m), 83 | Keycode::N => with_mod("n", m), 84 | Keycode::O => with_mod("o", m), 85 | Keycode::P => with_mod("p", m), 86 | Keycode::Q => with_mod("q", m), 87 | Keycode::R => with_mod("r", m), 88 | Keycode::S => with_mod("s", m), 89 | Keycode::T => with_mod("t", m), 90 | Keycode::U => with_mod("u", m), 91 | Keycode::V => with_mod("v", m), 92 | Keycode::W => with_mod("w", m), 93 | Keycode::X => with_mod("x", m), 94 | Keycode::Y => with_mod("y", m), 95 | Keycode::Z => with_mod("z", m), 96 | // Numerical 97 | Keycode::Num0 => with_mod("0", m), 98 | Keycode::Num1 => with_mod("1", m), 99 | Keycode::Num2 => with_mod("2", m), 100 | Keycode::Num3 => with_mod("3", m), 101 | Keycode::Num4 => with_mod("4", m), 102 | Keycode::Num5 => with_mod("5", m), 103 | Keycode::Num6 => with_mod("6", m), 104 | Keycode::Num7 => with_mod("7", m), 105 | Keycode::Num8 => with_mod("8", m), 106 | Keycode::Num9 => with_mod("9", m), 107 | // Single-char 108 | Keycode::Ampersand => with_mod("&", m), 109 | Keycode::Asterisk => with_mod("*", m), 110 | Keycode::At => with_mod("@", m), 111 | Keycode::Backquote => with_mod("`", m), 112 | Keycode::Backslash => with_mod("\\", m), 113 | Keycode::Caret => with_mod("^", m), 114 | Keycode::Colon => with_mod(":", m), 115 | Keycode::Comma => with_mod(",", m), 116 | Keycode::Dollar => with_mod("$", m), 117 | Keycode::Equals => with_mod("=", m), 118 | Keycode::Exclaim => with_mod("!", m), 119 | Keycode::Greater => with_mod(">", m), 120 | Keycode::Hash => with_mod("#", m), 121 | Keycode::KpA => with_mod("a", m), 122 | Keycode::KpAmpersand => with_mod("&", m), 123 | Keycode::KpAt => with_mod("at", m), 124 | Keycode::KpB => with_mod("b", m), 125 | Keycode::KpC => with_mod("c", m), 126 | Keycode::KpColon => with_mod(":", m), 127 | Keycode::KpD => with_mod("D", m), 128 | Keycode::KpDblAmpersand => with_mod("&&", m), 129 | Keycode::KpDblVerticalBar => with_mod("||", m), 130 | Keycode::KpDecimal => with_mod(".", m), 131 | Keycode::KpE => with_mod("e", m), 132 | Keycode::KpExclam => with_mod("!", m), 133 | Keycode::KpF => with_mod("f", m), 134 | Keycode::KpGreater => with_mod(">", m), 135 | Keycode::KpHash => with_mod("#", m), 136 | Keycode::KpLeftBrace => with_mod("{", m), 137 | Keycode::KpLeftParen => with_mod("(", m), 138 | Keycode::KpPercent => with_mod("%", m), 139 | Keycode::KpPeriod => with_mod(".", m), 140 | Keycode::KpRightBrace => with_mod("}", m), 141 | Keycode::KpRightParen => with_mod(")", m), 142 | Keycode::KpVerticalBar => with_mod("|", m), 143 | Keycode::LeftBracket => with_mod("[", m), 144 | Keycode::LeftParen => with_mod("(", m), 145 | Keycode::Minus => with_mod("-", m), 146 | Keycode::Percent => with_mod("%", m), 147 | Keycode::Period => with_mod(".", m), 148 | Keycode::Plus => with_mod("+", m), 149 | Keycode::Question => with_mod("?", m), 150 | Keycode::Quote => with_mod("'", m), 151 | Keycode::Quotedbl => with_mod("\"", m), 152 | Keycode::RightBracket => with_mod("]", m), 153 | Keycode::RightParen => with_mod(")", m), 154 | Keycode::Semicolon => with_mod(";", m), 155 | Keycode::Slash => with_mod("/", m), 156 | Keycode::Underscore => with_mod("_", m), 157 | // Special-repr 158 | Keycode::AcHome => with_mod("", m), 159 | Keycode::Backspace => with_mod("", m), 160 | Keycode::Delete => with_mod("", m), 161 | Keycode::Down => with_mod("", m), 162 | Keycode::End => with_mod("", m), 163 | Keycode::Escape => with_mod("", m), 164 | Keycode::F1 => with_mod("", m), 165 | Keycode::F2 => with_mod("", m), 166 | Keycode::F3 => with_mod("", m), 167 | Keycode::F4 => with_mod("", m), 168 | Keycode::F5 => with_mod("", m), 169 | Keycode::F6 => with_mod("", m), 170 | Keycode::F7 => with_mod("", m), 171 | Keycode::F8 => with_mod("", m), 172 | Keycode::F9 => with_mod("", m), 173 | Keycode::F10 => with_mod("<10>", m), 174 | Keycode::F11 => with_mod("<11>", m), 175 | Keycode::F12 => with_mod("<12>", m), 176 | Keycode::F13 => with_mod("<13>", m), 177 | Keycode::F14 => with_mod("<14>", m), 178 | Keycode::F15 => with_mod("<15>", m), 179 | Keycode::F16 => with_mod("<16>", m), 180 | Keycode::F17 => with_mod("<17>", m), 181 | Keycode::F18 => with_mod("<18>", m), 182 | Keycode::F19 => with_mod("<19>", m), 183 | Keycode::F20 => with_mod("<20>", m), 184 | Keycode::F21 => with_mod("<21>", m), 185 | Keycode::F22 => with_mod("<22>", m), 186 | Keycode::F23 => with_mod("<23>", m), 187 | Keycode::F24 => with_mod("<24>", m), 188 | Keycode::Help => with_mod("", m), 189 | Keycode::Home => with_mod("", m), 190 | Keycode::Insert => with_mod("", m), 191 | Keycode::Kp0 => with_mod("", m), 192 | Keycode::Kp1 => with_mod("", m), 193 | Keycode::Kp2 => with_mod("", m), 194 | Keycode::Kp3 => with_mod("", m), 195 | Keycode::Kp4 => with_mod("", m), 196 | Keycode::Kp5 => with_mod("", m), 197 | Keycode::Kp6 => with_mod("", m), 198 | Keycode::Kp7 => with_mod("", m), 199 | Keycode::Kp8 => with_mod("", m), 200 | Keycode::Kp9 => with_mod("", m), 201 | Keycode::Kp00 => with_mod("", m), 202 | Keycode::Kp000 => with_mod("", m), 203 | Keycode::KpBackspace => with_mod("", m), 204 | Keycode::KpComma => with_mod("", m), 205 | Keycode::KpDivide => with_mod("", m), 206 | Keycode::KpEnter => with_mod("", m), 207 | Keycode::KpEquals => with_mod("", m), 208 | Keycode::KpEqualsAS400 => with_mod("", m), 209 | Keycode::KpLess => with_mod("", m), 210 | Keycode::KpMinus => with_mod("", m), 211 | Keycode::KpMultiply => with_mod("", m), 212 | Keycode::KpPlus => with_mod("", m), 213 | Keycode::Left => with_mod("", m), 214 | Keycode::Less => with_mod("", m), 215 | Keycode::PageDown => with_mod("", m), 216 | Keycode::PageUp => with_mod("", m), 217 | Keycode::Return => with_mod("", m), 218 | Keycode::Return2 => with_mod("", m), 219 | Keycode::Right => with_mod("", m), 220 | Keycode::Space => with_mod("", m), 221 | Keycode::Tab => with_mod("", m), 222 | Keycode::Undo => with_mod("", m), 223 | Keycode::Up => with_mod("", m), 224 | // No repr 225 | _ => None, 226 | } 227 | } else { 228 | None 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod keys; 2 | 3 | use swayipc::{Connection, NodeLayout}; 4 | 5 | use std::process::Command; 6 | 7 | use std::collections::vec_deque::Drain; 8 | use std::collections::HashMap; 9 | use std::collections::VecDeque; 10 | use std::convert::TryFrom; 11 | use std::convert::TryInto; 12 | use std::env; 13 | use std::string::String; 14 | use std::time::Instant; 15 | 16 | extern crate sdl2; 17 | 18 | use sdl2::event::{Event, WindowEvent}; 19 | use sdl2::pixels::Color; 20 | use sdl2::pixels::PixelFormatEnum; 21 | use sdl2::rect::Rect; 22 | use sdl2::render::BlendMode; 23 | use sdl2::render::Canvas; 24 | use sdl2::render::Texture; 25 | use sdl2::render::TextureCreator; 26 | use sdl2::video::Window; 27 | use sdl2::video::WindowContext; 28 | use sdl2::VideoSubsystem; 29 | 30 | use neovim_lib::{Neovim, NeovimApi, Session, UiAttachOptions, Value}; 31 | 32 | type AtlasIndexKey = char; 33 | type NvimRow = usize; 34 | type NvimColumn = usize; 35 | type NvimWidth = usize; 36 | type NvimHeight = usize; 37 | type NvimGridId = u64; 38 | type NvimWinId = u64; 39 | 40 | #[derive(Debug)] 41 | pub enum SplitDirection { 42 | Above = 0, 43 | Below = 1, 44 | Left = 2, 45 | Right = 3, 46 | BelowRight = 4, 47 | AboveLeft = 5, 48 | TopLeft = 6, 49 | BottomRight = 7, 50 | Previous = 8, 51 | } 52 | 53 | impl TryFrom for SplitDirection { 54 | type Error = (); 55 | 56 | fn try_from(v: u64) -> Result { 57 | match v { 58 | x if x == SplitDirection::Above as u64 => Ok(SplitDirection::Above), 59 | x if x == SplitDirection::Below as u64 => Ok(SplitDirection::Below), 60 | x if x == SplitDirection::Left as u64 => Ok(SplitDirection::Left), 61 | x if x == SplitDirection::Right as u64 => Ok(SplitDirection::Right), 62 | x if x == SplitDirection::BelowRight as u64 => Ok(SplitDirection::BelowRight), 63 | x if x == SplitDirection::AboveLeft as u64 => Ok(SplitDirection::AboveLeft), 64 | x if x == SplitDirection::TopLeft as u64 => Ok(SplitDirection::TopLeft), 65 | x if x == SplitDirection::BottomRight as u64 => Ok(SplitDirection::BottomRight), 66 | x if x == SplitDirection::Previous as u64 => Ok(SplitDirection::Previous), 67 | _ => Err(()), 68 | } 69 | } 70 | } 71 | 72 | #[derive(Debug)] 73 | enum Damage { 74 | Cell { 75 | row: NvimRow, 76 | column: NvimColumn, 77 | width: NvimWidth, 78 | height: NvimHeight, 79 | }, 80 | Destroy {}, 81 | VerticalScroll { 82 | to_row: NvimRow, 83 | from_row: NvimRow, 84 | height: NvimHeight, 85 | from_col: NvimColumn, 86 | width: NvimWidth, 87 | }, 88 | } 89 | 90 | pub struct NvimGrid { 91 | chars: Vec>>, 92 | colors: Vec>, 93 | cursor: (NvimRow, NvimColumn), 94 | damages: Vec, 95 | window_id: NvimWinId, 96 | } 97 | 98 | impl NvimGrid { 99 | pub fn new(width: NvimWidth, height: NvimHeight) -> NvimGrid { 100 | NvimGrid { 101 | chars: vec![vec![Some(' '); width]; height], 102 | colors: vec![vec![0; width]; height], 103 | cursor: (0, 0), 104 | damages: vec![], 105 | window_id: 0, 106 | } 107 | } 108 | pub fn get_height(&self) -> NvimHeight { 109 | assert!(self.chars.len() == self.colors.len()); 110 | self.chars.len() as NvimHeight 111 | } 112 | pub fn get_width(&self) -> NvimWidth { 113 | (if self.chars.len() < 1 { 114 | 0 115 | } else { 116 | assert!(self.chars[0].len() == self.colors[0].len()); 117 | self.chars[0].len() 118 | }) as NvimWidth 119 | } 120 | pub fn get_cursor_pos(&self) -> (NvimRow, NvimColumn) { 121 | let w = self.get_width(); 122 | let h = self.get_height(); 123 | match (self.cursor.0 < h, self.cursor.1 < w) { 124 | (true, true) => (self.cursor.0, self.cursor.1), 125 | (true, false) => (self.cursor.0, self.get_width() - 1), 126 | (false, true) => (self.get_height() - 1, self.cursor.1), 127 | _ => (if h > 0 { h - 1 } else { 0 }, if w > 0 { w - 1 } else { 0 }), 128 | } 129 | } 130 | pub fn set_cursor_pos(&mut self, row: NvimRow, column: NvimColumn) { 131 | self.cursor.0 = row; 132 | self.cursor.1 = column; 133 | } 134 | } 135 | 136 | fn to_sdl_color(color: u64) -> Color { 137 | Color::RGB( 138 | ((color & 0x00ff_0000) >> 16) as u8, 139 | ((color & 0x0000_ff00) >> 8) as u8, 140 | (color & 0x0000_00ff) as u8, 141 | ) 142 | } 143 | 144 | pub struct NvimHighlightAttribute { 145 | background: Option, 146 | foreground: Option, 147 | special: Option, 148 | blend: u8, 149 | bold: bool, 150 | italic: bool, 151 | reverse: bool, 152 | strikethrough: bool, 153 | undercurl: bool, 154 | underline: bool, 155 | } 156 | 157 | impl NvimHighlightAttribute { 158 | pub fn new() -> NvimHighlightAttribute { 159 | NvimHighlightAttribute { 160 | background: None, 161 | foreground: None, 162 | special: None, 163 | blend: 0, 164 | bold: false, 165 | italic: false, 166 | reverse: false, 167 | strikethrough: false, 168 | undercurl: false, 169 | underline: false, 170 | } 171 | } 172 | } 173 | 174 | pub struct NvimState { 175 | grids: HashMap, 176 | hl_attrs: HashMap, 177 | cursor_grid: NvimGridId, 178 | cmdline_content: String, 179 | cmdline_firstc: char, 180 | cmdline_pos: u64, 181 | cmdline_prompt: String, 182 | cmdline_shown: bool, 183 | cursor_on: bool, 184 | message_attrs: Vec, 185 | message_contents: Vec, 186 | message_time: Instant, 187 | has_moved_since_last_message: bool, 188 | } 189 | 190 | impl NvimState { 191 | pub fn new() -> NvimState { 192 | NvimState { 193 | grids: HashMap::new(), 194 | hl_attrs: HashMap::new(), 195 | cursor_grid: 0, 196 | cmdline_content: String::new(), 197 | cmdline_firstc: ' ', 198 | cmdline_pos: 0, 199 | cmdline_prompt: String::new(), 200 | cmdline_shown: false, 201 | cursor_on: true, 202 | message_attrs: vec![], 203 | message_contents: vec![], 204 | message_time: Instant::now(), 205 | has_moved_since_last_message: false, 206 | } 207 | } 208 | pub fn cmdline_hide(&mut self) { 209 | self.cmdline_shown = false; 210 | } 211 | pub fn cmdline_pos(&mut self, pos: u64, _level: u64) { 212 | self.cmdline_pos = pos; 213 | } 214 | pub fn cmdline_show( 215 | &mut self, 216 | content: &Vec, 217 | pos: u64, 218 | firstc: &str, 219 | prompt: &str, 220 | _indent: u64, 221 | _level: u64, 222 | ) { 223 | self.cmdline_content = content.into_iter().fold("".to_string(), |s, v| { 224 | s + if let Some(a) = v.as_array() { 225 | a[1].as_str().or(Some("")).unwrap() 226 | } else { 227 | "" 228 | } 229 | }); 230 | self.cmdline_firstc = firstc.chars().next().or(Some(' ')).unwrap(); 231 | self.cmdline_pos = pos; 232 | self.cmdline_prompt = prompt.to_string(); 233 | self.cmdline_shown = true; 234 | } 235 | pub fn default_colors_set( 236 | &mut self, 237 | rgb_fg: Option, 238 | rgb_bg: Option, 239 | rgb_sp: Option, 240 | ) { 241 | let id = 0; 242 | let high = if let Some(a) = self.hl_attrs.get_mut(&id) { 243 | a 244 | } else { 245 | self.hl_attrs.insert(id, NvimHighlightAttribute::new()); 246 | self.hl_attrs.get_mut(&id).unwrap() 247 | }; 248 | high.foreground = rgb_fg.map(|c| to_sdl_color(c)); 249 | high.background = rgb_bg.map(|c| to_sdl_color(c)); 250 | high.special = rgb_sp.map(|c| to_sdl_color(c)); 251 | for (_, g) in self.grids.iter_mut() { 252 | g.damages.push(Damage::Cell { 253 | row: 0, 254 | column: 0, 255 | width: g.get_width(), 256 | height: g.get_height(), 257 | }); 258 | } 259 | } 260 | pub fn grid_clear(&mut self, id: NvimGridId) { 261 | if let Some(grid) = self.grids.get_mut(&id) { 262 | for row in 0..grid.get_height() { 263 | for column in 0..grid.get_width() { 264 | grid.chars[row][column] = None; 265 | grid.colors[row][column] = 0; 266 | } 267 | } 268 | } 269 | } 270 | pub fn grid_destroy(&mut self, id: NvimGridId) { 271 | if let Some(grid) = self.grids.get_mut(&id) { 272 | grid.damages.push(Damage::Destroy {}); 273 | } 274 | } 275 | pub fn grid_cursor_goto(&mut self, id: NvimGridId, row: NvimRow, column: NvimColumn) { 276 | self.cursor_grid = id; 277 | if let Some(grid) = self.grids.get_mut(&id) { 278 | let old_pos = grid.get_cursor_pos(); 279 | grid.set_cursor_pos(row, column); 280 | grid.damages.push(Damage::Cell { 281 | row: old_pos.0, 282 | column: old_pos.1, 283 | width: 1, 284 | height: 1, 285 | }); 286 | self.has_moved_since_last_message = true; 287 | } 288 | } 289 | pub fn grid_resize(&mut self, id: NvimGridId, width: NvimWidth, height: NvimHeight) { 290 | let grid = if let Some(g) = self.grids.get_mut(&id) { 291 | g 292 | } else { 293 | self.grids.insert(id, NvimGrid::new(0, 0)); 294 | self.grids.get_mut(&id).unwrap() 295 | }; 296 | if grid.get_height() > height { 297 | grid.chars.truncate(height); 298 | grid.colors.truncate(height); 299 | } else { 300 | // grid.damages.push(Damage::Cell { 301 | // row: grid.get_height(), 302 | // column: 0, 303 | // width, 304 | // height: height - grid.get_height(), 305 | // }); 306 | for _count in grid.get_height()..height { 307 | grid.chars.push(vec![None; width]); 308 | grid.colors.push(vec![0; width]); 309 | } 310 | } 311 | if grid.get_width() != width { 312 | // if grid.get_width() < width { 313 | // grid.damages.push(Damage::Cell { 314 | // row: 0, 315 | // column: grid.get_width(), 316 | // width: width - grid.get_width(), 317 | // height: grid.get_height(), 318 | // }); 319 | // } 320 | for row in 0..grid.get_height() { 321 | grid.chars[row].resize(width as usize, Some(' ')); 322 | grid.colors[row].resize(width as usize, 0); 323 | } 324 | } 325 | grid.damages.push(Damage::Cell { 326 | row: 0, 327 | column: 0, 328 | width: grid.get_width(), 329 | height: grid.get_height(), 330 | }); 331 | } 332 | pub fn grid_line( 333 | &mut self, 334 | id: NvimGridId, 335 | row: NvimRow, 336 | col_start: NvimColumn, 337 | cells: &Vec, 338 | ) { 339 | let grid = self.grids.get_mut(&id).unwrap(); 340 | let chars = &mut grid.chars[row as usize]; 341 | let colors = &mut grid.colors[row as usize]; 342 | let mut prev_column = col_start as usize; 343 | let mut prev_color = 0; 344 | let mut damage_length: NvimWidth = 0; 345 | for cell in cells { 346 | let mut c = cell.as_array().unwrap().into_iter(); 347 | let char = c.next().unwrap(); 348 | if let Some(Value::Integer(color)) = c.next() { 349 | prev_color = color.as_u64().unwrap(); 350 | } 351 | let repeat = (if let Some(Value::Integer(r)) = c.next() { 352 | r.as_u64().unwrap() 353 | } else { 354 | 1 355 | }) as NvimWidth; 356 | for _times in 0..repeat { 357 | chars[prev_column] = char.as_str().unwrap().chars().next(); 358 | colors[prev_column] = prev_color; 359 | prev_column += 1; 360 | } 361 | damage_length += repeat; 362 | } 363 | grid.damages.push(Damage::Cell { 364 | row, 365 | column: col_start, 366 | width: damage_length, 367 | height: 1, 368 | }); 369 | } 370 | pub fn grid_scroll( 371 | &mut self, 372 | id: NvimGridId, 373 | top: NvimRow, 374 | bot: NvimRow, 375 | left: NvimColumn, 376 | right: NvimColumn, 377 | rows: i64, 378 | _cols: i64, 379 | ) { 380 | assert!(_cols == 0); 381 | if let Some(grid) = self.grids.get_mut(&id) { 382 | if rows > 0 { 383 | // Moving characters up 384 | let r: usize = rows as usize; 385 | let bottom = if (bot + r) >= grid.get_height() { 386 | grid.get_height() - r 387 | } else { 388 | bot 389 | }; 390 | for y in top..bottom { 391 | for x in left..right { 392 | grid.chars[y][x] = grid.chars[y + r][x]; 393 | grid.colors[y][x] = grid.colors[y + r][x]; 394 | } 395 | } 396 | grid.damages.push(Damage::VerticalScroll { 397 | from_row: top + r, 398 | to_row: top, 399 | height: bottom - top, 400 | from_col: left, 401 | width: right - left, 402 | }); 403 | } else if rows < 0 { 404 | // Moving characters down 405 | let mut y = bot - 1; 406 | while y >= top && ((y as i64) + rows) >= 0 { 407 | for x in left..right { 408 | grid.chars[y][x] = grid.chars[((y as i64) + rows) as usize][x]; 409 | grid.colors[y][x] = grid.colors[((y as i64) + rows) as usize][x]; 410 | } 411 | y -= 1 412 | } 413 | grid.damages.push(Damage::VerticalScroll { 414 | from_row: top, 415 | to_row: top + (rows.abs() as usize), 416 | height: bot - top - (rows.abs() as usize), 417 | from_col: left, 418 | width: right - left, 419 | }); 420 | } 421 | } 422 | } 423 | pub fn hl_attr_define(&mut self, id: u64, map: &Vec<(Value, Value)>) { 424 | let attr = if let Some(a) = self.hl_attrs.get_mut(&id) { 425 | a 426 | } else { 427 | self.hl_attrs.insert(id, NvimHighlightAttribute::new()); 428 | self.hl_attrs.get_mut(&id).unwrap() 429 | }; 430 | for (k, v) in map { 431 | let key = k.as_str().or(Some("")).unwrap(); 432 | match key { 433 | "foreground" => { 434 | attr.foreground = v.as_u64().map(|c| to_sdl_color(c)); 435 | } 436 | "background" => { 437 | attr.background = v.as_u64().map(|c| to_sdl_color(c)); 438 | } 439 | "special" => { 440 | attr.special = v.as_u64().map(|c| to_sdl_color(c)); 441 | } 442 | "blend" => { 443 | attr.blend = v.as_u64().unwrap() as u8; 444 | } 445 | "reverse" => { 446 | attr.reverse = v.as_bool().unwrap(); 447 | } 448 | "italic" => { 449 | attr.italic = v.as_bool().unwrap(); 450 | } 451 | "bold" => { 452 | attr.bold = v.as_bool().unwrap(); 453 | } 454 | "strikethrough" => { 455 | attr.strikethrough = v.as_bool().unwrap(); 456 | } 457 | "underline" => { 458 | attr.underline = v.as_bool().unwrap(); 459 | } 460 | "undercurl" => { 461 | attr.undercurl = v.as_bool().unwrap(); 462 | } 463 | "fg_indexed" => { 464 | // Do nothing, only useful for UIs running in terminals 465 | } 466 | _ => { 467 | println!("Unsupported hl attr key {} in {:?}", key, map); 468 | } 469 | } 470 | } 471 | } 472 | pub fn msg_clear(&mut self) { 473 | self.message_attrs.truncate(0); 474 | self.message_contents.truncate(0); 475 | } 476 | pub fn msg_show(&mut self, _kind: &str, content: &Vec, _replace_last: bool) { 477 | for c in content { 478 | if let Some(arr) = c.as_array() { 479 | let mut args = arr.into_iter(); 480 | self.message_attrs 481 | .push(args.next().map_or(0, |v| v.as_u64().or(Some(0)).unwrap())); 482 | self.message_contents 483 | .push(args.next().map_or("".to_string(), |v| v.as_str().or(Some("")).unwrap().to_string())); 484 | } 485 | } 486 | self.message_time = Instant::now(); 487 | self.has_moved_since_last_message = false; 488 | } 489 | pub fn win_hide(&mut self, sway: &mut Connection, win: NvimWinId) { 490 | let title = format!("Nwin - Grid {}", win); 491 | // Find the parent node of the window being split 492 | let parent_node = sway 493 | .get_tree() 494 | .unwrap() 495 | .find(|node| { 496 | for n in &node.nodes { 497 | if let Some(str) = &n.name { 498 | if str == &title { 499 | return true; 500 | } 501 | } 502 | } 503 | false 504 | }) 505 | .unwrap(); 506 | if parent_node.layout != NodeLayout::Tabbed { 507 | let node = parent_node 508 | .find(|n| { 509 | if let Some(str) = &n.name { 510 | return str == &title; 511 | } 512 | false 513 | }) 514 | .unwrap(); 515 | sway.run_command(format!("[con_id={}] splitv", node.id)) 516 | .unwrap(); 517 | sway.run_command(format!("[con_id={}] layout tabbed", node.id)) 518 | .unwrap(); 519 | } 520 | } 521 | pub fn win_pos( 522 | &mut self, 523 | grid: NvimGridId, 524 | win: NvimWinId, 525 | _start_row: NvimRow, 526 | _start_col: NvimHeight, 527 | _width: NvimWidth, 528 | _height: NvimHeight, 529 | ) { 530 | let grid = self.grids.get_mut(&grid).unwrap(); 531 | grid.window_id = win; 532 | } 533 | pub fn win_split( 534 | &mut self, 535 | sway: &mut Connection, 536 | _win1: NvimWinId, 537 | grid1: NvimGridId, 538 | _win2: NvimWinId, 539 | _grid2: NvimGridId, 540 | flags: SplitDirection, 541 | ) { 542 | let (split_command, desired_sway_layout) = match flags { 543 | SplitDirection::Above | SplitDirection::Below => ("splitv", NodeLayout::SplitV), 544 | _ => ("splith", NodeLayout::SplitH), 545 | }; 546 | let title = format!("Nwin - Grid {}", grid1); 547 | // Find the parent node of the window being split 548 | let parent_node = sway 549 | .get_tree() 550 | .unwrap() 551 | .find(|node| { 552 | for n in &node.nodes { 553 | if let Some(str) = &n.name { 554 | if str == &title { 555 | return true; 556 | } 557 | } 558 | } 559 | false 560 | }) 561 | .unwrap(); 562 | if parent_node.layout != desired_sway_layout { 563 | let node = parent_node 564 | .find(|n| { 565 | if let Some(str) = &n.name { 566 | return str == &title; 567 | } 568 | false 569 | }) 570 | .unwrap(); 571 | let command = format!("[con_id={}] {}", node.id, split_command); 572 | sway.run_command(command).unwrap(); 573 | } 574 | } 575 | } 576 | 577 | fn do_redraw(state: &mut NvimState, sway: &mut Connection, args: Drain<'_, Value>) { 578 | for update_events in args { 579 | if let Value::Array(update_events) = update_events { 580 | let mut update_events_iter = update_events.into_iter(); 581 | if let Some(event_name) = update_events_iter.next() { 582 | if let Some(str) = event_name.as_str() { 583 | for events in update_events_iter { 584 | let arr = events.as_array(); 585 | match str { 586 | "busy_start" => { 587 | state.cursor_on = false; 588 | } 589 | "busy_stop" => { 590 | state.cursor_on = true; 591 | } 592 | "cmdline_hide" => { 593 | state.cmdline_hide(); 594 | } 595 | "cmdline_pos" => { 596 | let mut args = arr.unwrap().into_iter(); 597 | state.cmdline_pos( 598 | args.next().unwrap().as_u64().unwrap(), 599 | args.next().unwrap().as_u64().unwrap(), 600 | ); 601 | } 602 | "cmdline_show" => { 603 | let mut args = arr.unwrap().into_iter(); 604 | state.cmdline_show( 605 | args.next().unwrap().as_array().unwrap(), 606 | args.next().unwrap().as_u64().unwrap(), 607 | args.next().unwrap().as_str().unwrap(), 608 | args.next().unwrap().as_str().unwrap(), 609 | args.next().unwrap().as_u64().unwrap(), 610 | args.next().unwrap().as_u64().unwrap(), 611 | ); 612 | } 613 | "default_colors_set" => { 614 | let mut args = arr.unwrap().into_iter(); 615 | state.default_colors_set( 616 | args.next().map(|v| v.as_u64().unwrap()), 617 | args.next().map(|v| v.as_u64().unwrap()), 618 | args.next().map(|v| v.as_u64().unwrap()), 619 | ); 620 | } 621 | "grid_clear" => { 622 | let mut args = arr.unwrap().into_iter(); 623 | state.grid_clear( 624 | args.next().unwrap().as_u64().unwrap() as NvimGridId 625 | ); 626 | } 627 | "grid_destroy" => { 628 | let mut args = arr.unwrap().into_iter(); 629 | state.grid_destroy( 630 | args.next().unwrap().as_u64().unwrap() as NvimGridId 631 | ); 632 | } 633 | "grid_cursor_goto" => { 634 | let mut args = arr.unwrap().into_iter(); 635 | state.grid_cursor_goto( 636 | args.next().unwrap().as_u64().unwrap() as NvimGridId, 637 | args.next().unwrap().as_u64().unwrap() as NvimRow, 638 | args.next().unwrap().as_u64().unwrap() as NvimColumn, 639 | ); 640 | } 641 | "grid_line" => { 642 | let mut args = arr.unwrap().into_iter(); 643 | let grid = args.next().unwrap().as_u64().unwrap() as NvimGridId; 644 | let row = args.next().unwrap().as_u64().unwrap() as NvimRow; 645 | let col_start = 646 | args.next().unwrap().as_u64().unwrap() as NvimColumn; 647 | if let Value::Array(cells) = args.next().unwrap() { 648 | state.grid_line(grid, row, col_start, &cells); 649 | } 650 | } 651 | "grid_resize" => { 652 | let mut args = arr.unwrap().into_iter(); 653 | state.grid_resize( 654 | args.next().unwrap().as_u64().unwrap() as NvimGridId, 655 | args.next().unwrap().as_u64().unwrap() as NvimWidth, 656 | args.next().unwrap().as_u64().unwrap() as NvimHeight, 657 | ); 658 | } 659 | "grid_scroll" => { 660 | let mut args = arr.unwrap().into_iter(); 661 | state.grid_scroll( 662 | args.next().unwrap().as_u64().unwrap() as NvimGridId, 663 | args.next().unwrap().as_u64().unwrap() as NvimRow, 664 | args.next().unwrap().as_u64().unwrap() as NvimRow, 665 | args.next().unwrap().as_u64().unwrap() as NvimColumn, 666 | args.next().unwrap().as_u64().unwrap() as NvimColumn, 667 | args.next().unwrap().as_i64().unwrap(), 668 | args.next().unwrap().as_i64().unwrap(), 669 | ); 670 | } 671 | "hl_attr_define" => { 672 | let mut args = arr.unwrap().into_iter(); 673 | state.hl_attr_define( 674 | args.next().unwrap().as_u64().unwrap(), 675 | args.next().unwrap().as_map().unwrap(), 676 | ); 677 | } 678 | "msg_clear" => { 679 | state.msg_clear(); 680 | } 681 | "msg_show" => { 682 | let mut args = arr.unwrap().into_iter(); 683 | state.msg_show( 684 | args.next().unwrap().as_str().unwrap(), 685 | args.next().unwrap().as_array().unwrap(), 686 | args.next().unwrap().as_bool().unwrap(), 687 | ) 688 | } 689 | "win_hide" => { 690 | let mut args = arr.unwrap().into_iter(); 691 | state.win_hide( 692 | sway, 693 | args.next().unwrap().as_u64().unwrap() as NvimWinId, 694 | ); 695 | } 696 | "win_pos" => { 697 | let mut args = arr.unwrap().into_iter(); 698 | let grid_id = args.next().unwrap().as_u64().unwrap() as NvimGridId; 699 | // neovim-lib doesn't unpack ext types so we end up having to do it 700 | // ourselves. Pretty stupid... 701 | // https://github.com/msgpack/msgpack/blob/master/spec.md#extension-types 702 | // https://github.com/msgpack/msgpack/blob/master/spec.md#int-format-family 703 | let (t, values) = args.next().unwrap().as_ext().unwrap(); 704 | // 1 is the id for the window type, 0xCD means 16bit integer 705 | assert!(t == 1 && values[0] == 0xCD && values.len() == 3); 706 | let win_id = 707 | (values[1] as NvimWinId) << 8 | (values[2] as NvimWinId); 708 | state.win_pos( 709 | grid_id, 710 | win_id as NvimWinId, 711 | args.next().unwrap().as_u64().unwrap() as NvimRow, 712 | args.next().unwrap().as_u64().unwrap() as NvimColumn, 713 | args.next().unwrap().as_u64().unwrap() as NvimWidth, 714 | args.next().unwrap().as_u64().unwrap() as NvimHeight, 715 | ); 716 | } 717 | "win_split" => { 718 | let mut args = arr.unwrap().into_iter(); 719 | state.win_split( 720 | sway, 721 | args.next().unwrap().as_u64().unwrap(), 722 | args.next().unwrap().as_u64().unwrap(), 723 | args.next().unwrap().as_u64().unwrap(), 724 | args.next().unwrap().as_u64().unwrap(), 725 | args.next().unwrap().as_u64().unwrap().try_into().unwrap(), 726 | ); 727 | } 728 | "flush" | "hl_group_set" | "mode_info_set" | "mode_change" 729 | | "mouse_off" | "option_set" | "win_viewport" | "msg_showcmd" 730 | | "msg_showmode" => {} 731 | _ => { 732 | println!("Unhandled {}, {:?}", str, events); 733 | } 734 | } 735 | } 736 | } else { 737 | eprintln!("Found non-str event name!"); 738 | } 739 | } else { 740 | eprintln!("No event name!"); 741 | } 742 | } else { 743 | eprintln!("Unsupported event type {:?}", update_events); 744 | } 745 | } 746 | } 747 | 748 | struct SDLGrid { 749 | canvas: Canvas, 750 | atlas: Texture, 751 | atlas_index: HashMap, 752 | atlas_next_slot: i32, 753 | big_texture: Texture, 754 | big_texture_copy: Texture, 755 | texture_creator: TextureCreator, 756 | width: u32, // pixels 757 | height: u32, // pixels 758 | grid_x_offset: u32, 759 | grid_y_offset: u32, 760 | font_width: u32, 761 | font_height: u32, 762 | } 763 | 764 | fn find_sdl_gl_driver() -> Option { 765 | for (index, item) in sdl2::render::drivers().enumerate() { 766 | if item.name == "opengl" { 767 | return Some(index as u32); 768 | } 769 | } 770 | None 771 | } 772 | 773 | static MAX_TEXTURE_SIZE : u32 = 16384; 774 | 775 | impl SDLGrid { 776 | pub fn new( 777 | video_subsystem: &VideoSubsystem, 778 | id: NvimGridId, 779 | font_width: u32, 780 | font_height: u32, 781 | ) -> SDLGrid { 782 | let title = format!("Nwin - Grid {}", id); 783 | let width = 1; 784 | let height = 1; 785 | let window = video_subsystem 786 | .window(&title, width, height) 787 | .opengl() 788 | .resizable() 789 | .build() 790 | .unwrap(); 791 | let canvas = window 792 | .into_canvas() 793 | .index(find_sdl_gl_driver().unwrap()) 794 | .build() 795 | .unwrap(); 796 | let texture_creator = canvas.texture_creator(); 797 | let big_texture = texture_creator 798 | .create_texture_target(PixelFormatEnum::ARGB8888, width, height) 799 | .unwrap(); 800 | let big_texture_copy = texture_creator 801 | .create_texture_target(PixelFormatEnum::ARGB8888, width, height) 802 | .unwrap(); 803 | let mut atlas = texture_creator 804 | .create_texture_target(PixelFormatEnum::ARGB8888, MAX_TEXTURE_SIZE, font_height) 805 | .unwrap(); 806 | atlas.set_blend_mode(BlendMode::Blend); 807 | SDLGrid { 808 | canvas, 809 | atlas, 810 | atlas_index: HashMap::new(), 811 | atlas_next_slot: 0, 812 | big_texture, 813 | big_texture_copy, 814 | texture_creator, 815 | width, 816 | height, 817 | grid_x_offset: 0, 818 | grid_y_offset: 0, 819 | font_width, 820 | font_height, 821 | } 822 | } 823 | } 824 | 825 | const WHITE : Color = Color::RGBA(255,255,255,255); 826 | const TRANSPARENT : Color = Color::RGBA(200,0,128,0); 827 | 828 | const REF: &str = include_str!("../.git/HEAD"); 829 | const REF_MASTER: &str = include_str!("../.git/refs/heads/master"); 830 | 831 | pub fn main() -> Result<(), String> { 832 | env::remove_var("NVIM_LISTEN_ADDRESS"); 833 | 834 | let mut sway = Connection::new().unwrap(); 835 | 836 | // Create the command used to run neovim. We swallow the arguments we understand and forward 837 | // the rest to neovim. 838 | let mut neovim_command = Command::new("nvim"); 839 | neovim_command.args(&["--embed", "--cmd", "let g:started_by_nwin = v:true"]); 840 | let mut print_fps = false; 841 | let mut max_fps = 60; 842 | for argument in env::args().skip(1) { 843 | if argument == "--print-fps" { 844 | print_fps = true; 845 | } else if argument.starts_with("--max-fps=") { 846 | max_fps = argument.get(10..).unwrap().parse::().unwrap(); 847 | } else { 848 | neovim_command.arg(argument); 849 | } 850 | } 851 | 852 | // Create the neovim session 853 | let session = Session::new_child_cmd(&mut neovim_command).unwrap(); 854 | let mut nvim = Neovim::new(session); 855 | let mut state = NvimState::new(); 856 | let chan = nvim.session.start_event_loop_channel(); 857 | 858 | let commit = if REF.starts_with("ref: refs/heads/master") { 859 | REF_MASTER 860 | } else { 861 | REF 862 | }.trim(); 863 | // Advertise UI name 864 | nvim.set_client_info( 865 | "nwin", 866 | vec![ 867 | ("major".into(), "0".into()), 868 | ("minor".into(), "1".into()), 869 | ("patch".into(), "0".into()), 870 | ("commit".into(), commit.into()), 871 | ], 872 | "ui", 873 | vec![], 874 | vec![], 875 | ) 876 | .unwrap(); 877 | 878 | // Two things: retrieve neovim channel and figure out if server supports ext_win 879 | let chan_id; 880 | let mut has_ext_windows = false; 881 | if let Ok(info) = nvim.get_api_info() { 882 | chan_id = info[0].as_u64().unwrap(); 883 | for (key, value) in info[1].as_map().unwrap() { 884 | if key.as_str().unwrap() == "ui_options" { 885 | for option in value.as_array().unwrap() { 886 | if option.as_str().unwrap() == "ext_windows" { 887 | has_ext_windows = true; 888 | } 889 | } 890 | } 891 | } 892 | } else { 893 | panic!("nvim_get_api_info() failed!"); 894 | } 895 | 896 | // Use channel id to get warned when server closes. 897 | let command = format!( 898 | "autocmd VimLeave * call rpcnotify({}, 'nwin_vimleave')", 899 | chan_id 900 | ); 901 | nvim.command(&command).unwrap(); 902 | 903 | let sdl_context = sdl2::init()?; 904 | let video_subsystem = sdl_context.video()?; 905 | let ttf_context = sdl2::ttf::init().map_err(|e| e.to_string())?; 906 | 907 | // use home crate to figure out path to ~/downloads/NotoSansMono/NotoSansMono-Regular.ttf 908 | let mut _fontpath = String::new(); 909 | match home::home_dir() { 910 | // this might not be a good way.. 911 | Some(path) => _fontpath.push_str(&path.to_string_lossy()), 912 | None => println!("can't find font to use, check README.md"), 913 | } 914 | _fontpath.push_str("/downloads/NotoSansMono/NotoSansMono-Regular.ttf"); 915 | 916 | let font = ttf_context.load_font(_fontpath.to_string(), 16)?; 917 | 918 | let mut font_width = 1; 919 | let mut font_height = 1; 920 | 921 | let mut sdl_grids: HashMap = HashMap::new(); 922 | 923 | // We need to know the size of the first window in order to be able to attach the neovim GUI 924 | // So we cheat and create an SDLGrid for grid id 2 which we know will be the first "buffer" 925 | // grid id neovim creates when ext_multigrid is present. 926 | // We then use this SDLGrid to compute the different sizes we need and then attach 927 | { 928 | let grid_id = if has_ext_windows { 2 } else { 1 }; 929 | sdl_grids.insert( 930 | grid_id, 931 | SDLGrid::new( 932 | &video_subsystem, 933 | grid_id, 934 | font_width, 935 | font_height, 936 | ), 937 | ); 938 | let the_grid = sdl_grids.get_mut(&grid_id).unwrap(); 939 | 940 | let surface = font 941 | .render("A") 942 | .blended(Color::RGBA(255, 0, 0, 255)) 943 | .map_err(|e| e.to_string())?; 944 | let texture = the_grid 945 | .texture_creator 946 | .create_texture_from_surface(&surface) 947 | .map_err(|e| e.to_string())?; 948 | let t = texture.query(); 949 | font_width = t.width; 950 | font_height = t.height; 951 | the_grid.font_width = font_width; 952 | the_grid.font_height = font_height; 953 | 954 | let size = the_grid.canvas.window().size(); 955 | the_grid.width = size.0; 956 | the_grid.height = size.0; 957 | 958 | let row_count = the_grid.width / the_grid.font_width; 959 | let col_count = the_grid.height / the_grid.font_height; 960 | let mut options = UiAttachOptions::new(); 961 | options.set_rgb(true); 962 | options.set_linegrid_external(true); 963 | if has_ext_windows { 964 | options 965 | .set_messages_external(true) 966 | .set_multigrid(true) 967 | .set_windows_external(true); 968 | } else { 969 | println!( 970 | "Warning: neovim server does not support external windows. Continuing without." 971 | ); 972 | } 973 | nvim.ui_attach(80, 20, &options).unwrap(); 974 | 975 | the_grid.grid_x_offset = (the_grid.width - (col_count * the_grid.font_width)) / 2; 976 | the_grid.grid_y_offset = (the_grid.height - (row_count * the_grid.font_height)) / 2; 977 | 978 | the_grid.big_texture = the_grid 979 | .texture_creator 980 | .create_texture_target(PixelFormatEnum::ARGB8888, the_grid.width, the_grid.height) 981 | .unwrap(); 982 | the_grid.big_texture_copy = the_grid 983 | .texture_creator 984 | .create_texture_target(PixelFormatEnum::ARGB8888, the_grid.width, the_grid.height) 985 | .unwrap(); 986 | the_grid.atlas = the_grid 987 | .texture_creator 988 | .create_texture_target(PixelFormatEnum::ARGB8888, MAX_TEXTURE_SIZE, the_grid.font_height) 989 | .unwrap(); 990 | the_grid.atlas.set_blend_mode(BlendMode::Blend); 991 | } 992 | 993 | let mut event_pump = sdl_context.event_pump().map_err(|e| e.to_string())?; 994 | 995 | let mut cursor_rect = Rect::new(0, 0, 0, 0); 996 | let mut redraw_messages = VecDeque::new(); 997 | let mut last_frame_check = Instant::now(); 998 | let mut last_second = Instant::now(); 999 | let mut frame_count = 0; 1000 | let mut grids_to_destroy = vec![]; 1001 | 1002 | // Note: this can't be inside of loop because we might sometimes draw a frame between two 1003 | // events that have the same timestamp. 1004 | let mut last_keydown_timestamp = 0; 1005 | 1006 | 'running: loop { 1007 | grids_to_destroy.truncate(0); 1008 | let now = Instant::now(); 1009 | // 1) Process events from neovim 1010 | while let Ok((str, messages)) = chan.try_recv() { 1011 | if str == "redraw" { 1012 | // Copy messages into the vecdequeue, remember position of last flush if there's 1013 | // one. 1014 | let len = messages.len(); 1015 | let mut i = 0; 1016 | let mut last_flush_position = None; 1017 | for msg in messages { 1018 | if let Value::Array(ref events) = msg { 1019 | if let Some(str) = events.into_iter().next() { 1020 | if let Some(str) = str.as_str() { 1021 | if str == "flush" { 1022 | last_flush_position = Some(len - i); 1023 | } 1024 | } 1025 | } 1026 | } 1027 | i += 1; 1028 | redraw_messages.push_back(msg); 1029 | } 1030 | if let Some(pos) = last_flush_position { 1031 | do_redraw( 1032 | &mut state, 1033 | &mut sway, 1034 | redraw_messages.drain(0..redraw_messages.len() - pos), 1035 | ); 1036 | } 1037 | } else if str == "nwin_vimleave" { 1038 | break 'running; 1039 | } else { 1040 | eprintln!("Unexpected message: {}", str); 1041 | } 1042 | } 1043 | 1044 | if last_frame_check.elapsed().as_secs() >= 60 { 1045 | eprintln!("No frame for more than a minute. Resetting atlas."); 1046 | for (key, grid) in state.grids.iter_mut() { 1047 | let s = sdl_grids.get_mut(key).unwrap(); 1048 | s.atlas_index.clear(); 1049 | s.atlas_next_slot = 0; 1050 | grid.damages.push( 1051 | Damage::Cell { 1052 | row: 0, 1053 | column: 0, 1054 | width: grid.get_width(), 1055 | height: grid.get_height(), 1056 | } 1057 | ); 1058 | } 1059 | } 1060 | last_frame_check = Instant::now(); 1061 | 1062 | // 3) Redraw grid damages 1063 | if let Some(default_hl) = state.hl_attrs.get(&0) { 1064 | let default_bg = default_hl.background; 1065 | let default_fg = default_hl.foreground; 1066 | for (key, grid) in state.grids.iter_mut() { 1067 | if has_ext_windows && *key == 1 { 1068 | grid.damages.truncate(0); 1069 | continue; 1070 | } 1071 | let SDLGrid { 1072 | canvas, 1073 | atlas, 1074 | atlas_index, 1075 | atlas_next_slot, 1076 | big_texture, 1077 | big_texture_copy, 1078 | texture_creator, 1079 | width, 1080 | height, 1081 | grid_x_offset, 1082 | grid_y_offset, 1083 | font_width, 1084 | font_height, 1085 | .. 1086 | } = if let Some(g) = sdl_grids.get_mut(key) { 1087 | g 1088 | } else { 1089 | sdl_grids.insert( 1090 | *key, 1091 | SDLGrid::new(&video_subsystem, *key, font_width, font_height), 1092 | ); 1093 | sdl_grids.get_mut(key).unwrap() 1094 | }; 1095 | // Perform any resize 1096 | { 1097 | let size = canvas.window().size(); 1098 | if size.0 != *width || size.1 != *height { 1099 | let col_count = size.0 / *font_width; 1100 | let row_count = size.1 / *font_height; 1101 | let pixel_grid_width = col_count * *font_width; 1102 | let pixel_grid_height = row_count * *font_height; 1103 | let new_x_offset = (size.0 - pixel_grid_width) / 2; 1104 | let new_y_offset = (size.1 - pixel_grid_height) / 2; 1105 | if (col_count as usize) != grid.get_width() 1106 | || (row_count as usize) != grid.get_height() 1107 | { 1108 | // Let neovim know size changed 1109 | if let Err(e) = nvim.ui_try_resize_grid( 1110 | i64::try_from(*key).unwrap(), 1111 | col_count.into(), 1112 | row_count.into(), 1113 | ) { 1114 | eprintln!("{}", e); 1115 | } 1116 | } 1117 | // Resize sdl grid 1118 | let min_width = std::cmp::min(size.0, *width); 1119 | let min_height = std::cmp::min(size.1, *height); 1120 | // back up big_texture to big_texture_copy 1121 | let backup_rectangle = Rect::new(0, 0, min_width, min_height); 1122 | canvas 1123 | .with_texture_canvas(big_texture_copy, |canvas| { 1124 | let from = Rect::new( 1125 | *grid_x_offset as i32, 1126 | *grid_y_offset as i32, 1127 | min_width, 1128 | min_height, 1129 | ); 1130 | canvas.copy(big_texture, from, backup_rectangle).unwrap(); 1131 | }) 1132 | .unwrap(); 1133 | // deallocate big_texture 1134 | // drop(big_texture); 1135 | // allocate new big_texture 1136 | *big_texture = texture_creator 1137 | .create_texture_target(None, size.0, size.1) 1138 | .unwrap(); 1139 | // restore backup 1140 | canvas 1141 | .with_texture_canvas(big_texture, |canvas| { 1142 | canvas.set_draw_color(default_bg.unwrap()); 1143 | canvas.clear(); 1144 | let to = Rect::new( 1145 | new_x_offset as i32, 1146 | new_y_offset as i32, 1147 | min_width, 1148 | min_height, 1149 | ); 1150 | canvas.copy(big_texture_copy, backup_rectangle, to).unwrap(); 1151 | }) 1152 | .unwrap(); 1153 | // destroy backup buffer 1154 | // drop(big_texture_copy); 1155 | // allocate new backup buffer 1156 | *big_texture_copy = texture_creator 1157 | .create_texture_target(None, size.0, size.1) 1158 | .unwrap(); 1159 | *width = size.0; 1160 | *height = size.1; 1161 | *grid_x_offset = new_x_offset; 1162 | *grid_y_offset = new_y_offset; 1163 | } 1164 | } 1165 | 1166 | if grid.get_width() > 0 && grid.get_height() > 0 { 1167 | for d in &grid.damages { 1168 | if let Damage::Cell { 1169 | row, 1170 | column, 1171 | width, 1172 | height, 1173 | } = d 1174 | { 1175 | let damage_top = *row; 1176 | let mut damage_bottom = row + height; 1177 | if damage_bottom > grid.get_height() { 1178 | damage_bottom = grid.get_height(); 1179 | } 1180 | for current_row in damage_top..damage_bottom { 1181 | let damage_left = *column; 1182 | let mut damage_right = column + width; 1183 | if damage_right > grid.get_width() { 1184 | damage_right = grid.get_width(); 1185 | } 1186 | for current_column in damage_left..damage_right { 1187 | let atlas_key = grid.chars[current_row][current_column] 1188 | .or_else(|| Some(0 as char)) 1189 | .unwrap(); 1190 | let attr_id = grid.colors[current_row][current_column]; 1191 | if let None = atlas_index.get(&atlas_key) { 1192 | canvas 1193 | .with_texture_canvas(atlas, |canvas| { 1194 | if let Some(char) = 1195 | grid.chars[current_row][current_column] 1196 | { 1197 | let surface = font 1198 | .render(&char.to_string()) 1199 | .blended(WHITE) 1200 | .map_err(|e| e.to_string()) 1201 | .unwrap(); 1202 | let cell_rect = Rect::new( 1203 | *atlas_next_slot, 1204 | 0, 1205 | surface.width(), 1206 | surface.height(), 1207 | ); 1208 | let glyph = surface 1209 | .into_canvas() 1210 | .unwrap(); 1211 | let mut texture = texture_creator 1212 | .create_texture_target(PixelFormatEnum::ARGB8888, cell_rect.width(), cell_rect.height()) 1213 | .unwrap(); 1214 | texture.update(None, &glyph.read_pixels(None, PixelFormatEnum::ARGB8888).unwrap(), 4 * cell_rect.width() as usize).unwrap(); 1215 | canvas.set_draw_color(TRANSPARENT); 1216 | canvas.fill_rect(cell_rect).unwrap(); 1217 | canvas.copy(&texture, None, cell_rect).unwrap(); 1218 | atlas_index.insert( 1219 | atlas_key, 1220 | (*atlas_next_slot, cell_rect.width()), 1221 | ); 1222 | *atlas_next_slot += cell_rect.width() as i32; 1223 | if *atlas_next_slot > (MAX_TEXTURE_SIZE as i32) { 1224 | eprintln!("Texture atlas is full!"); 1225 | } 1226 | } else { 1227 | let cell_rect = Rect::new( 1228 | *atlas_next_slot, 1229 | 0, 1230 | *font_width, 1231 | *font_height, 1232 | ); 1233 | canvas.set_draw_color(TRANSPARENT); 1234 | canvas.fill_rect(cell_rect).unwrap(); 1235 | 1236 | atlas_index.insert( 1237 | atlas_key, 1238 | (*atlas_next_slot, *font_width), 1239 | ); 1240 | *atlas_next_slot += *font_width as i32; 1241 | if *atlas_next_slot > (MAX_TEXTURE_SIZE as i32) { 1242 | eprintln!("Texture atlas is full!"); 1243 | } 1244 | } 1245 | }) 1246 | .unwrap(); 1247 | } 1248 | let hl_attr = state.hl_attrs.get(&attr_id).unwrap(); 1249 | let (pos, width) = atlas_index.get(&atlas_key).unwrap(); 1250 | canvas 1251 | .with_texture_canvas(big_texture, |canvas| { 1252 | let mut bg = hl_attr 1253 | .background 1254 | .or_else(|| default_bg) 1255 | .unwrap(); 1256 | let mut fg = hl_attr 1257 | .foreground 1258 | .or_else(|| default_fg) 1259 | .unwrap(); 1260 | if hl_attr.reverse { 1261 | let tmp = bg; 1262 | bg = fg; 1263 | fg = tmp; 1264 | } 1265 | 1266 | let from = Rect::new(*pos, 0, *width, *font_height); 1267 | let to = Rect::new( 1268 | (*grid_x_offset as i32) 1269 | + (current_column as i32) 1270 | * (*font_width as i32), 1271 | (*grid_y_offset as i32) 1272 | + (current_row as i32) * (*font_height as i32), 1273 | *width, 1274 | *font_height, 1275 | ); 1276 | canvas.set_draw_color(bg); 1277 | canvas.fill_rect(to).unwrap(); 1278 | atlas.set_color_mod(fg.r, fg.g, fg.b); 1279 | canvas.copy(&atlas, from, to).unwrap(); 1280 | }) 1281 | .unwrap(); 1282 | } 1283 | } 1284 | } else if let Damage::VerticalScroll { from_row, to_row, height, from_col, width } = d { 1285 | canvas 1286 | .with_texture_canvas(big_texture_copy, |canvas| { 1287 | canvas.copy(&big_texture, None, None).unwrap(); 1288 | }) 1289 | .unwrap(); 1290 | canvas 1291 | .with_texture_canvas(big_texture, |canvas| { 1292 | let f = Rect::new( 1293 | (*from_col as i32) * (*font_width as i32), 1294 | (*grid_y_offset as i32) 1295 | + (*from_row as i32) * (*font_height as i32), 1296 | (*width as u32) * (*font_width as u32), 1297 | (*height as u32) * (*font_height as u32), 1298 | ); 1299 | let t = Rect::new( 1300 | (*from_col as i32) * (*font_width as i32), 1301 | (*grid_y_offset as i32) 1302 | + (*to_row as i32) * (*font_height as i32), 1303 | (*width as u32) * (*font_width as u32), 1304 | (*height as u32) * (*font_height as u32), 1305 | ); 1306 | canvas.copy(&big_texture_copy, f, t).unwrap(); 1307 | }) 1308 | .unwrap(); 1309 | } else if let Damage::Destroy {} = d { 1310 | grids_to_destroy.push(*key); 1311 | } 1312 | } 1313 | let r = Rect::new(0, 0, *width, *height); 1314 | canvas.copy(&big_texture, r, r).unwrap(); 1315 | 1316 | if *key == state.cursor_grid { 1317 | if state.cmdline_shown { 1318 | canvas.set_draw_color(default_bg.unwrap()); 1319 | let cmdline_rect = Rect::new(0, 0, *width, *font_height); 1320 | canvas.fill_rect(cmdline_rect).unwrap(); 1321 | let s = state.cmdline_firstc.to_string() + &state.cmdline_content; 1322 | let msg = font 1323 | .render(&s) 1324 | .blended(default_fg.unwrap()) 1325 | .map_err(|e| e.to_string()) 1326 | .unwrap(); 1327 | let texture = texture_creator 1328 | .create_texture_from_surface(&msg) 1329 | .map_err(|e| e.to_string()) 1330 | .unwrap(); 1331 | let q = texture.query(); 1332 | canvas 1333 | .copy(&texture, None, Rect::new(0, 0, q.width, q.height)) 1334 | .unwrap(); 1335 | } else if state.cursor_on { 1336 | let (row, column) = grid.get_cursor_pos(); 1337 | let attr_id = grid.colors[row as usize][column as usize]; 1338 | if let Some(hl_attr) = state.hl_attrs.get(&attr_id) { 1339 | canvas.set_draw_color( 1340 | hl_attr.foreground.or_else(|| default_fg).unwrap(), 1341 | ); 1342 | cursor_rect.set_x( 1343 | (*grid_x_offset as i32) 1344 | + (column as i32) * (*font_width as i32), 1345 | ); 1346 | cursor_rect.set_y( 1347 | (*grid_y_offset as i32) + (row as i32) * (*font_height as i32), 1348 | ); 1349 | cursor_rect.set_width(*font_width); 1350 | cursor_rect.set_height(*font_height); 1351 | canvas.fill_rect(cursor_rect).unwrap(); 1352 | if let Some(c) = grid.chars[row as usize][column as usize] { 1353 | let (pos, width) = atlas_index.get(&c).unwrap(); 1354 | let color = hl_attr.background.or_else(|| default_bg).unwrap(); 1355 | atlas.set_color_mod(color.r, color.g, color.b); 1356 | let from = Rect::new(*pos, 0, *width, *font_height); 1357 | canvas.copy(&atlas, from, cursor_rect).unwrap(); 1358 | } 1359 | } 1360 | } 1361 | for i in 0..state.message_contents.len() { 1362 | if let Some(attr) = state.hl_attrs.get(&state.message_attrs[i]) { 1363 | let s = &state.message_contents[i]; 1364 | let msg = font 1365 | .render(&s) 1366 | .shaded( 1367 | attr.foreground.or_else(|| default_fg).unwrap(), 1368 | attr.background.or_else(|| default_bg).unwrap(), 1369 | ) 1370 | .map_err(|e| e.to_string()) 1371 | .unwrap(); 1372 | let texture = texture_creator 1373 | .create_texture_from_surface(&msg) 1374 | .map_err(|e| e.to_string()) 1375 | .unwrap(); 1376 | let q = texture.query(); 1377 | canvas 1378 | .copy( 1379 | &texture, 1380 | None, 1381 | Rect::new( 1382 | 0, 1383 | (i as i32) * (q.height as i32), 1384 | q.width, 1385 | q.height, 1386 | ), 1387 | ) 1388 | .unwrap(); 1389 | } 1390 | } 1391 | } 1392 | } 1393 | canvas.present(); 1394 | if print_fps { 1395 | frame_count += 1; 1396 | if last_second.elapsed().as_secs() > 0 { 1397 | println!("{} fps", frame_count); 1398 | frame_count = 0; 1399 | last_second = Instant::now(); 1400 | } 1401 | } 1402 | grid.damages.truncate(0); 1403 | } 1404 | let time_since_last_message = (Instant::now() - state.message_time).as_millis(); 1405 | if state.has_moved_since_last_message && time_since_last_message > 3000 { 1406 | state.msg_clear(); 1407 | } 1408 | for key in &grids_to_destroy { 1409 | sdl_grids.remove(&key); 1410 | state.grids.remove(&key); 1411 | } 1412 | } 1413 | 1414 | // Use the time we have left before having to display the next frame to read events from 1415 | // ui and forward them to neovim if necessary. 1416 | let mut time_left = (1000 / max_fps) - i64::try_from(now.elapsed().as_millis()).map_or(0, |v| v); 1417 | while time_left > 1 { 1418 | let mut input_string = "".to_owned(); 1419 | if let Some(event) = event_pump.wait_event_timeout(time_left as u32) { 1420 | match event { 1421 | Event::Quit { .. } => { 1422 | nvim.quit_no_save().unwrap(); 1423 | break 'running; 1424 | } 1425 | Event::KeyDown { timestamp, .. } => { 1426 | if let Some(str) = keys::nvim_event_representation(event) { 1427 | input_string.push_str(&str); 1428 | last_keydown_timestamp = timestamp; 1429 | } 1430 | } 1431 | Event::TextInput { timestamp, text: ref s, .. } => { 1432 | if timestamp != last_keydown_timestamp { 1433 | for c in s.chars() { 1434 | // NOTE: We ignore space because it has a non-literal repr and it's better 1435 | // to have it go through the keydown nvim.input, in order to be able to 1436 | // handle both and (we can't tell from a 1437 | // TextInput event). 1438 | if c != ' ' { 1439 | if let Some(s) = keys::nvim_char_representation(c) { 1440 | input_string.push_str(s); 1441 | } else { 1442 | input_string.push_str(&c.to_string()); 1443 | } 1444 | } 1445 | } 1446 | } 1447 | } 1448 | Event::Window { 1449 | window_id, 1450 | win_event, 1451 | .. 1452 | } => { 1453 | // When a window closes down, Hidden and FocusLost are sent, but we've 1454 | // already gotten rid of the grid, so we won't be able to find it in sdl_grids. 1455 | // That's why we let Some(...) = instead of .unwrap()'ing. 1456 | if let Some((key, _)) = sdl_grids 1457 | .iter_mut() 1458 | .find(|(_, v)| v.canvas.window().id() == window_id) 1459 | { 1460 | match win_event { 1461 | WindowEvent::Close => { 1462 | if let Some(grid) = state.grids.get(key) { 1463 | let window_id = grid.window_id; 1464 | if let Err(_) = nvim.call_function( 1465 | "nvim_win_close", 1466 | vec![window_id.into(), true.into()], 1467 | ) { 1468 | eprintln!("nvim_win_close({})", window_id); 1469 | } 1470 | } 1471 | } 1472 | WindowEvent::FocusLost => { 1473 | if let Err(_) = nvim.command("doautocmd FocusGained") { 1474 | eprintln!("doautocmd FocusLost failed"); 1475 | } 1476 | } 1477 | WindowEvent::FocusGained => { 1478 | if let Err(_) = nvim.command("doautocmd FocusGained") { 1479 | eprintln!("doautocmd FocusGained failed"); 1480 | } 1481 | // Can't unwrap because on app startup we'll have an os window but 1482 | // no neovim window 1483 | if let Some(grid) = state.grids.get(key) { 1484 | if let Err(_) = nvim.call_function( 1485 | "nvim_set_current_win", 1486 | vec![grid.window_id.into()], 1487 | ) { 1488 | eprintln!("nvim_set_current_win({}) failed", grid.window_id); 1489 | } 1490 | } 1491 | } 1492 | _ => {} 1493 | } 1494 | } 1495 | } 1496 | _ => {} 1497 | } 1498 | } 1499 | if input_string != "" { 1500 | if let Err(_) = nvim.input(&input_string) { 1501 | eprintln!("nvim_input('{}') failed", &input_string); 1502 | } 1503 | } 1504 | time_left = (1000 / max_fps) - i64::try_from(now.elapsed().as_millis()).map_or(0, |v| v); 1505 | } 1506 | } 1507 | 1508 | Ok(()) 1509 | } 1510 | --------------------------------------------------------------------------------