cmd.rs
1 use crate::autocomplete::{self, Autocomplete, FileCompleter, FileCompleterOpts}; 2 use crate::brush::BrushMode; 3 use crate::history::History; 4 use crate::parser::*; 5 use crate::platform; 6 use crate::session::{Direction, Input, Mode, PanState, Tool, VisualState}; 7 8 use memoir::traits::Parse; 9 use memoir::*; 10 11 use crate::gfx::Rect; 12 use crate::gfx::Rgba8; 13 14 use std::fmt; 15 use std::path::Path; 16 17 pub const COMMENT: char = '-'; 18 19 #[derive(Clone, PartialEq, Debug)] 20 pub enum Op { 21 Incr, 22 Decr, 23 Set(f32), 24 } 25 26 #[derive(Clone, Debug, PartialEq)] 27 pub enum Axis { 28 Horizontal, 29 Vertical, 30 } 31 32 /// User command. Most of the interactions available to 33 /// the user are modeled as commands that are processed 34 /// by the session. 35 #[derive(PartialEq, Debug, Clone)] 36 pub enum Command { 37 // Brush 38 Brush, 39 BrushSet(BrushMode), 40 BrushToggle(BrushMode), 41 BrushSize(Op), 42 BrushUnset(BrushMode), 43 44 #[allow(dead_code)] 45 Crop(Rect<u32>), 46 ChangeDir(Option<String>), 47 Echo(Value), 48 49 // Files 50 Edit(Vec<String>), 51 EditFrames(Vec<String>), 52 Export(Option<u32>, String), 53 Write(Option<String>), 54 WriteFrames(Option<String>), 55 WriteQuit, 56 Quit, 57 QuitAll, 58 ForceQuit, 59 ForceQuitAll, 60 Source(Option<String>), 61 62 // Frames 63 FrameAdd, 64 FrameClone(i32), 65 FrameRemove, 66 FramePrev, 67 FrameNext, 68 FrameResize(u32, u32), 69 70 // Palette 71 PaletteAdd(Rgba8), 72 PaletteClear, 73 PaletteGradient(Rgba8, Rgba8, usize), 74 PaletteSample, 75 PaletteSort, 76 PaletteWrite(String), 77 78 // Navigation 79 Pan(i32, i32), 80 Zoom(Op), 81 82 PaintColor(Rgba8, i32, i32), 83 PaintForeground(i32, i32), 84 PaintBackground(i32, i32), 85 PaintPalette(usize, i32, i32), 86 PaintLine(Rgba8, i32, i32, i32, i32), 87 88 // Selection 89 SelectionMove(i32, i32), 90 SelectionResize(i32, i32), 91 SelectionOffset(i32, i32), 92 SelectionExpand, 93 SelectionPaste, 94 SelectionYank, 95 SelectionCut, 96 SelectionFill(Option<Rgba8>), 97 SelectionErase, 98 SelectionJump(Direction), 99 SelectionFlip(Axis), 100 101 // Settings 102 Set(String, Value), 103 Toggle(String), 104 Reset, 105 Map(Box<KeyMapping>), 106 MapClear, 107 108 Slice(Option<usize>), 109 Fill(Option<Rgba8>), 110 111 SwapColors, 112 113 Mode(Mode), 114 Tool(Tool), 115 ToolPrev, 116 117 Undo, 118 Redo, 119 120 // View 121 ViewCenter, 122 ViewNext, 123 ViewPrev, 124 125 Noop, 126 } 127 128 impl Command { 129 pub fn repeats(&self) -> bool { 130 matches!( 131 self, 132 Self::Zoom(_) 133 | Self::BrushSize(_) 134 | Self::Pan(_, _) 135 | Self::Undo 136 | Self::Redo 137 | Self::ViewNext 138 | Self::ViewPrev 139 | Self::SelectionMove(_, _) 140 | Self::SelectionJump(_) 141 | Self::SelectionResize(_, _) 142 | Self::SelectionOffset(_, _) 143 ) 144 } 145 } 146 147 impl fmt::Display for Command { 148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 149 match self { 150 Self::Brush => write!(f, "Reset brush"), 151 Self::BrushSet(m) => write!(f, "Set brush mode to `{}`", m), 152 Self::BrushToggle(m) => write!(f, "Toggle `{}` brush mode", m), 153 Self::BrushSize(Op::Incr) => write!(f, "Increase brush size"), 154 Self::BrushSize(Op::Decr) => write!(f, "Decrease brush size"), 155 Self::BrushSize(Op::Set(s)) => write!(f, "Set brush size to {}", s), 156 Self::BrushUnset(m) => write!(f, "Unset brush `{}` mode", m), 157 Self::Crop(_) => write!(f, "Crop view"), 158 Self::ChangeDir(_) => write!(f, "Change the current working directory"), 159 Self::Echo(_) => write!(f, "Echo a value"), 160 Self::Edit(_) => write!(f, "Edit path(s)"), 161 Self::EditFrames(_) => write!(f, "Edit path(s) as animation frames"), 162 Self::Fill(Some(c)) => write!(f, "Fill view with {color}", color = c), 163 Self::Fill(None) => write!(f, "Fill view with background color"), 164 Self::ForceQuit => write!(f, "Quit view without saving"), 165 Self::ForceQuitAll => write!(f, "Quit all views without saving"), 166 Self::Map(_) => write!(f, "Map a key combination to a command"), 167 Self::MapClear => write!(f, "Clear all key mappings"), 168 Self::Mode(Mode::Help) => write!(f, "Toggle help"), 169 Self::Mode(m) => write!(f, "Switch to {} mode", m), 170 Self::FrameAdd => write!(f, "Add a blank frame to the view"), 171 Self::FrameClone(i) => write!(f, "Clone frame {} and add it to the view", i), 172 Self::FrameRemove => write!(f, "Remove the last frame of the view"), 173 Self::FramePrev => write!(f, "Navigate to previous frame"), 174 Self::FrameNext => write!(f, "Navigate to next frame"), 175 Self::Noop => write!(f, "No-op"), 176 Self::PaletteAdd(c) => write!(f, "Add {color} to palette", color = c), 177 Self::PaletteClear => write!(f, "Clear palette"), 178 Self::PaletteGradient(cs, ce, n) => { 179 write!(f, "Create {n} colors gradient from {cs} to {ce}") 180 } 181 Self::PaletteSample => write!(f, "Sample palette from view"), 182 Self::PaletteSort => write!(f, "Sort palette colors"), 183 Self::Pan(x, 0) if *x > 0 => write!(f, "Pan workspace right"), 184 Self::Pan(x, 0) if *x < 0 => write!(f, "Pan workspace left"), 185 Self::Pan(0, y) if *y > 0 => write!(f, "Pan workspace up"), 186 Self::Pan(0, y) if *y < 0 => write!(f, "Pan workspace down"), 187 Self::Pan(x, y) => write!(f, "Pan workspace by {},{}", x, y), 188 Self::Quit => write!(f, "Quit active view"), 189 Self::QuitAll => write!(f, "Quit all views"), 190 Self::Redo => write!(f, "Redo view edit"), 191 Self::FrameResize(_, _) => write!(f, "Resize active view frame"), 192 Self::Tool(Tool::Pan(_)) => write!(f, "Pan tool"), 193 Self::Tool(Tool::Brush) => write!(f, "Brush tool"), 194 Self::Tool(Tool::Sampler) => write!(f, "Color sampler tool"), 195 Self::Tool(Tool::FloodFill) => write!(f, "Flood fill tool"), 196 Self::ToolPrev => write!(f, "Switch to previous tool"), 197 Self::Set(s, v) => write!(f, "Set {setting} to {val}", setting = s, val = v), 198 Self::Slice(Some(n)) => write!(f, "Slice view into {} frame(s)", n), 199 Self::Slice(None) => write!(f, "Reset view slices"), 200 Self::Source(_) => write!(f, "Source an rx script (eg. a palette)"), 201 Self::SwapColors => write!(f, "Swap foreground & background colors"), 202 Self::Toggle(s) => write!(f, "Toggle {setting} on/off", setting = s), 203 Self::Undo => write!(f, "Undo view edit"), 204 Self::ViewCenter => write!(f, "Center active view"), 205 Self::ViewNext => write!(f, "Go to next view"), 206 Self::ViewPrev => write!(f, "Go to previous view"), 207 Self::Write(None) => write!(f, "Write view to disk"), 208 Self::Write(Some(_)) => write!(f, "Write view to disk as..."), 209 Self::WriteQuit => write!(f, "Write file to disk and quit"), 210 Self::Zoom(Op::Incr) => write!(f, "Zoom in view"), 211 Self::Zoom(Op::Decr) => write!(f, "Zoom out view"), 212 Self::Zoom(Op::Set(z)) => write!(f, "Set view zoom to {:.1}", z), 213 Self::Reset => write!(f, "Reset all settings to default"), 214 Self::SelectionFill(None) => write!(f, "Fill selection with foreground color"), 215 Self::SelectionYank => write!(f, "Yank (copy) selection"), 216 Self::SelectionCut => write!(f, "Cut selection"), 217 Self::SelectionPaste => write!(f, "Paste selection"), 218 Self::SelectionExpand => write!(f, "Expand selection to frame"), 219 Self::SelectionOffset(1, 1) => write!(f, "Outset selection"), 220 Self::SelectionOffset(-1, -1) => write!(f, "Inset selection"), 221 Self::SelectionOffset(x, y) => write!(f, "Offset selection by {:2},{:2}", x, y), 222 Self::SelectionMove(x, 0) if *x > 0 => write!(f, "Move selection right"), 223 Self::SelectionMove(x, 0) if *x < 0 => write!(f, "Move selection left"), 224 Self::SelectionMove(0, y) if *y > 0 => write!(f, "Move selection up"), 225 Self::SelectionMove(0, y) if *y < 0 => write!(f, "Move selection down"), 226 Self::SelectionJump(Direction::Forward) => { 227 write!(f, "Move selection forward by one frame") 228 } 229 Self::SelectionJump(Direction::Backward) => { 230 write!(f, "Move selection backward by one frame") 231 } 232 Self::SelectionErase => write!(f, "Erase selection contents"), 233 Self::SelectionFlip(Axis::Horizontal) => write!(f, "Flip selection horizontally"), 234 Self::SelectionFlip(Axis::Vertical) => write!(f, "Flip selection vertically"), 235 Self::PaintColor(_, x, y) => write!(f, "Paint {:2},{:2}", x, y), 236 _ => write!(f, "..."), 237 } 238 } 239 } 240 241 impl From<Command> for String { 242 fn from(cmd: Command) -> Self { 243 match cmd { 244 Command::Brush => format!("brush"), 245 Command::BrushSet(m) => format!("brush/set {}", m), 246 Command::BrushSize(Op::Incr) => format!("brush/size +"), 247 Command::BrushSize(Op::Decr) => format!("brush/size -"), 248 Command::BrushSize(Op::Set(s)) => format!("brush/size {}", s), 249 Command::BrushUnset(m) => format!("brush/unset {}", m), 250 Command::Echo(_) => unimplemented!(), 251 Command::Edit(_) => unimplemented!(), 252 Command::Fill(Some(c)) => format!("v/fill {}", c), 253 Command::Fill(None) => format!("v/fill"), 254 Command::ForceQuit => format!("q!"), 255 Command::ForceQuitAll => format!("qa!"), 256 Command::Map(_) => format!("map <key> <command> {{<command>}}"), 257 Command::Mode(m) => format!("mode {}", m), 258 Command::FrameAdd => format!("f/add"), 259 Command::FrameClone(i) => format!("f/clone {}", i), 260 Command::FrameRemove => format!("f/remove"), 261 Command::Export(None, path) => format!("export {}", path), 262 Command::Export(Some(s), path) => format!("export @{}x {}", s, path), 263 Command::Noop => format!(""), 264 Command::PaletteAdd(c) => format!("p/add {}", c), 265 Command::PaletteClear => format!("p/clear"), 266 Command::PaletteWrite(_) => format!("p/write"), 267 Command::PaletteSample => format!("p/sample"), 268 Command::PaletteGradient(cs, ce, n) => format!("p/gradient {} {} {}", cs, ce, n), 269 Command::Pan(x, y) => format!("pan {} {}", x, y), 270 Command::Quit => format!("q"), 271 Command::Redo => format!("redo"), 272 Command::FrameResize(w, h) => format!("f/resize {} {}", w, h), 273 Command::Set(s, v) => format!("set {} = {}", s, v), 274 Command::Slice(Some(n)) => format!("slice {}", n), 275 Command::Slice(None) => format!("slice"), 276 Command::Source(Some(path)) => format!("source {}", path), 277 Command::SwapColors => format!("swap"), 278 Command::Toggle(s) => format!("toggle {}", s), 279 Command::Undo => format!("undo"), 280 Command::ViewCenter => format!("v/center"), 281 Command::ViewNext => format!("v/next"), 282 Command::ViewPrev => format!("v/prev"), 283 Command::Write(None) => format!("w"), 284 Command::Write(Some(path)) => format!("w {}", path), 285 Command::WriteQuit => format!("wq"), 286 Command::Zoom(Op::Incr) => format!("v/zoom +"), 287 Command::Zoom(Op::Decr) => format!("v/zoom -"), 288 Command::Zoom(Op::Set(z)) => format!("v/zoom {}", z), 289 _ => unimplemented!(), 290 } 291 } 292 } 293 294 /////////////////////////////////////////////////////////////////////////////// 295 296 #[derive(PartialEq, Debug, Clone)] 297 pub struct KeyMapping { 298 pub input: Input, 299 pub press: Command, 300 pub release: Option<Command>, 301 pub modes: Vec<Mode>, 302 } 303 304 impl KeyMapping { 305 pub fn parser(modes: &[Mode]) -> Parser<KeyMapping> { 306 let modes = modes.to_vec(); 307 308 // Prevent stack overflow. 309 let press = Parser::new( 310 move |input| Commands::default().parser().parse(input), 311 "<cmd>", 312 ); 313 314 // Prevent stack overflow. 315 let release = Parser::new( 316 move |input| { 317 if let Some(i) = input.bytes().position(|c| c == b'}') { 318 match Commands::default().parser().parse(&input[..i]) { 319 Ok((cmd, rest)) if rest.is_empty() => Ok((cmd, &input[i..])), 320 Ok((_, rest)) => { 321 Err((format!("expected {:?}, got {:?}", '}', rest).into(), rest)) 322 } 323 Err(err) => Err(err), 324 } 325 } else { 326 Err(("unclosed '{' delimiter".into(), input)) 327 } 328 }, 329 "<cmd>", 330 ); 331 332 let character = between('\'', '\'', character()) 333 .map(Input::Character) 334 .skip(whitespace()) 335 .then(press.clone()) 336 .map(|(input, press)| ((input, press), None)); 337 let key = param::<platform::Key>() 338 .map(Input::Key) 339 .skip(whitespace()) 340 .then(press) 341 .skip(optional(whitespace())) 342 .then(optional(between('{', '}', release))); 343 344 character 345 .or(key) 346 .map(move |((input, press), release)| KeyMapping { 347 input, 348 press, 349 release, 350 modes: modes.clone(), 351 }) 352 .label("<key> <cmd>") // TODO: We should provide the full command somehow. 353 } 354 } 355 356 //////////////////////////////////////////////////////////////////////////////// 357 358 #[derive(Clone, PartialEq, Debug)] 359 pub enum Value { 360 Bool(bool), 361 U32(u32), 362 U32Tuple(u32, u32), 363 F32Tuple(f32, f32), 364 F64(f64), 365 Str(String), 366 Ident(String), 367 Rgba8(Rgba8), 368 } 369 370 impl Value { 371 pub fn is_set(&self) -> bool { 372 if let Value::Bool(b) = self { 373 return *b; 374 } 375 panic!("expected {:?} to be a `bool`", self); 376 } 377 378 pub fn to_f64(&self) -> f64 { 379 if let Value::F64(n) = self { 380 return *n; 381 } 382 panic!("expected {:?} to be a `float`", self); 383 } 384 385 pub fn to_u64(&self) -> u64 { 386 if let Value::U32(n) = self { 387 return *n as u64; 388 } 389 panic!("expected {:?} to be a `uint`", self); 390 } 391 392 pub fn to_rgba8(&self) -> Rgba8 { 393 if let Value::Rgba8(rgba8) = self { 394 return *rgba8; 395 } 396 panic!("expected {:?} to be a `Rgba8`", self); 397 } 398 399 pub fn description(&self) -> &'static str { 400 match self { 401 Self::Bool(_) => "on / off", 402 Self::U32(_) => "positive integer, eg. 32", 403 Self::F64(_) => "float, eg. 1.33", 404 Self::U32Tuple(_, _) => "two positive integers, eg. 32, 48", 405 Self::F32Tuple(_, _) => "two floats , eg. 32.17, 48.29", 406 Self::Str(_) => "string, eg. \"fnord\"", 407 Self::Rgba8(_) => "color, eg. #ffff00", 408 Self::Ident(_) => "identifier, eg. fnord", 409 } 410 } 411 } 412 413 impl From<Value> for (u32, u32) { 414 fn from(other: Value) -> (u32, u32) { 415 if let Value::U32Tuple(x, y) = other { 416 return (x, y); 417 } 418 panic!("expected {:?} to be a `(u32, u32)`", other); 419 } 420 } 421 422 impl From<Value> for f32 { 423 fn from(other: Value) -> f32 { 424 if let Value::F64(x) = other { 425 return x as f32; 426 } 427 panic!("expected {:?} to be a `f64`", other); 428 } 429 } 430 431 impl From<Value> for f64 { 432 fn from(other: Value) -> f64 { 433 if let Value::F64(x) = other { 434 return x; 435 } 436 panic!("expected {:?} to be a `f64`", other); 437 } 438 } 439 440 impl fmt::Display for Value { 441 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 442 match self { 443 Value::Bool(true) => "on".fmt(f), 444 Value::Bool(false) => "off".fmt(f), 445 Value::U32(u) => u.fmt(f), 446 Value::F64(x) => x.fmt(f), 447 Value::U32Tuple(x, y) => write!(f, "{},{}", x, y), 448 Value::F32Tuple(x, y) => write!(f, "{},{}", x, y), 449 Value::Str(s) => s.fmt(f), 450 Value::Rgba8(c) => c.fmt(f), 451 Value::Ident(i) => i.fmt(f), 452 } 453 } 454 } 455 456 impl Parse for Value { 457 fn parser() -> Parser<Self> { 458 let str_val = quoted().map(Value::Str).label("<string>"); 459 let rgba8_val = color().map(Value::Rgba8); 460 let u32_tuple_val = tuple::<u32>(natural(), natural()).map(|(x, y)| Value::U32Tuple(x, y)); 461 let u32_val = natural::<u32>().map(Value::U32); 462 let f64_tuple_val = 463 tuple::<f32>(rational(), rational()).map(|(x, y)| Value::F32Tuple(x, y)); 464 let f64_val = rational::<f64>().map(Value::F64).label("0.0 .. 4096.0"); 465 let bool_val = string("on") 466 .value(Value::Bool(true)) 467 .or(string("off").value(Value::Bool(false))) 468 .label("on/off"); 469 let ident_val = identifier().map(Value::Ident); 470 471 greediest(vec![ 472 rgba8_val, 473 u32_tuple_val, 474 f64_tuple_val, 475 u32_val, 476 f64_val, 477 bool_val, 478 ident_val, 479 str_val, 480 ]) 481 .label("<value>") 482 } 483 } 484 485 //////////////////////////////////////////////////////////////////////////////// 486 487 pub struct CommandLine { 488 /// The history of commands entered. 489 pub history: History, 490 /// Command auto-complete. 491 pub autocomplete: Autocomplete<CommandCompleter>, 492 /// Input cursor position. 493 pub cursor: usize, 494 /// Parser. 495 pub parser: Parser<Command>, 496 /// Commands. 497 pub commands: Commands, 498 /// The current input string displayed to the user. 499 input: String, 500 /// File extensions supported. 501 extensions: Vec<String>, 502 } 503 504 impl CommandLine { 505 const MAX_INPUT: usize = 256; 506 507 pub fn new<P: AsRef<Path>>(cwd: P, history_path: P, extensions: &[&str]) -> Self { 508 let cmds = Commands::default(); 509 510 Self { 511 input: String::with_capacity(Self::MAX_INPUT), 512 cursor: 0, 513 parser: cmds.line_parser(), 514 commands: cmds, 515 history: History::new(history_path, 1024), 516 autocomplete: Autocomplete::new(CommandCompleter::new(cwd, extensions)), 517 extensions: extensions.iter().map(|e| (*e).into()).collect(), 518 } 519 } 520 521 pub fn set_cwd(&mut self, path: &Path) { 522 let exts: Vec<_> = self.extensions.iter().map(|s| s.as_str()).collect(); 523 self.autocomplete = Autocomplete::new(CommandCompleter::new(path, exts.as_slice())); 524 } 525 526 pub fn parse(&self, input: &str) -> Result<Command, Error> { 527 match self.parser.parse(input) { 528 Ok((cmd, _)) => Ok(cmd), 529 Err((err, _)) => Err(err), 530 } 531 } 532 533 pub fn input(&self) -> String { 534 self.input.clone() 535 } 536 537 pub fn is_empty(&self) -> bool { 538 self.input.is_empty() 539 } 540 541 pub fn history_prev(&mut self) { 542 let prefix = self.prefix(); 543 544 if let Some(entry) = self.history.prev(&prefix).map(str::to_owned) { 545 self.replace(&entry); 546 } 547 } 548 549 pub fn history_next(&mut self) { 550 let prefix = self.prefix(); 551 552 if let Some(entry) = self.history.next(&prefix).map(str::to_owned) { 553 self.replace(&entry); 554 } else { 555 self.reset(); 556 } 557 } 558 559 pub fn completion_next(&mut self) { 560 let prefix = self.prefix(); 561 562 if let Some((completion, range)) = self.autocomplete.next(&prefix, self.cursor) { 563 // Replace old completion with new one. 564 self.cursor = range.start + completion.len(); 565 self.input.replace_range(range, &completion); 566 } 567 } 568 569 pub fn cursor_backward(&mut self) -> Option<char> { 570 if let Some(c) = self.peek_back() { 571 let cursor = self.cursor - c.len_utf8(); 572 573 // Don't allow deleting the `:` prefix of the command. 574 if c != ':' || cursor > 0 { 575 self.cursor = cursor; 576 self.autocomplete.invalidate(); 577 return Some(c); 578 } 579 } 580 None 581 } 582 583 pub fn cursor_forward(&mut self) -> Option<char> { 584 if let Some(c) = self.input[self.cursor..].chars().next() { 585 self.cursor += c.len_utf8(); 586 self.autocomplete.invalidate(); 587 Some(c) 588 } else { 589 None 590 } 591 } 592 593 pub fn cursor_back(&mut self) { 594 if self.cursor > 1 { 595 self.cursor = 1; 596 self.autocomplete.invalidate(); 597 } 598 } 599 600 pub fn cursor_front(&mut self) { 601 self.cursor = self.input.len(); 602 } 603 604 pub fn putc(&mut self, c: char) { 605 if self.input.len() + c.len_utf8() > self.input.capacity() { 606 return; 607 } 608 self.input.insert(self.cursor, c); 609 self.cursor += c.len_utf8(); 610 self.autocomplete.invalidate(); 611 } 612 613 pub fn puts(&mut self, s: &str) { 614 // TODO: Check capacity. 615 self.input.push_str(s); 616 self.cursor += s.len(); 617 self.autocomplete.invalidate(); 618 } 619 620 pub fn delc(&mut self) { 621 match self.peek_back() { 622 // Don't allow deleting the ':' unless it's the last remaining character. 623 Some(c) if self.cursor > 1 || self.input.len() == 1 => { 624 self.cursor -= c.len_utf8(); 625 self.input.remove(self.cursor); 626 self.autocomplete.invalidate(); 627 } 628 _ => {} 629 } 630 } 631 632 pub fn clear(&mut self) { 633 self.cursor = 0; 634 self.input.clear(); 635 self.history.reset(); 636 self.autocomplete.invalidate(); 637 } 638 639 //////////////////////////////////////////////////////////////////////////// 640 641 fn replace(&mut self, s: &str) { 642 // We don't re-assign `input` here, because it 643 // has a fixed capacity we want to preserve. 644 self.input.clear(); 645 self.input.push_str(s); 646 self.autocomplete.invalidate(); 647 } 648 649 fn reset(&mut self) { 650 self.clear(); 651 self.putc(':'); 652 } 653 654 fn prefix(&self) -> String { 655 self.input[..self.cursor].to_owned() 656 } 657 658 #[cfg(test)] 659 fn peek(&self) -> Option<char> { 660 self.input[self.cursor..].chars().next() 661 } 662 663 fn peek_back(&self) -> Option<char> { 664 self.input[..self.cursor].chars().next_back() 665 } 666 } 667 668 pub struct Commands { 669 commands: Vec<(&'static str, &'static str, Parser<Command>)>, 670 } 671 672 impl Commands { 673 pub fn new() -> Self { 674 Self { 675 commands: vec![( 676 "#", 677 "Add color to palette", 678 color().map(Command::PaletteAdd), 679 )], 680 } 681 } 682 683 pub fn parser(&self) -> Parser<Command> { 684 use std::iter; 685 686 let noop = expect(|s| s.is_empty(), "<empty>").value(Command::Noop); 687 let commands = self.commands.iter().map(|(_, _, v)| v.clone()); 688 let choices = commands.chain(iter::once(noop)).collect(); 689 690 symbol(':') 691 .then( 692 choice(choices).or(peek( 693 until(hush(whitespace()).or(end())) 694 .try_map(|cmd| Err(format!("unknown command: {}", cmd))), 695 )), 696 ) 697 .map(|(_, cmd)| cmd) 698 } 699 700 pub fn line_parser(&self) -> Parser<Command> { 701 self.parser() 702 .skip(optional(whitespace())) 703 .skip(optional(comment())) 704 .end() 705 } 706 707 pub fn iter(&self) -> impl Iterator<Item = &(&'static str, &'static str, Parser<Command>)> { 708 self.commands.iter() 709 } 710 711 /////////////////////////////////////////////////////////////////////////// 712 713 fn command<F>(mut self, name: &'static str, help: &'static str, f: F) -> Self 714 where 715 F: Fn(Parser<String>) -> Parser<Command>, 716 { 717 let cmd = peek( 718 string(name) 719 .followed_by(hush(whitespace()) / end()) 720 .skip(optional(whitespace())), 721 ) 722 .label(name); 723 724 self.commands.push((name, help, f(cmd))); 725 self 726 } 727 } 728 729 impl Default for Commands { 730 fn default() -> Self { 731 Self::new() 732 .command("q", "Quit view", |p| p.value(Command::Quit)) 733 .command("qa", "Quit all views", |p| p.value(Command::QuitAll)) 734 .command("q!", "Force quit view", |p| p.value(Command::ForceQuit)) 735 .command("qa!", "Force quit all views", |p| { 736 p.value(Command::ForceQuitAll) 737 }) 738 .command("export", "Export view", |p| { 739 p.then(optional(scale().skip(whitespace())).then(path())) 740 .map(|(_, (scale, path))| Command::Export(scale, path)) 741 }) 742 .command("wq", "Write & quit view", |p| p.value(Command::WriteQuit)) 743 .command("x", "Write & quit view", |p| p.value(Command::WriteQuit)) 744 .command("w", "Write view", |p| { 745 p.then(optional(path())) 746 .map(|(_, path)| Command::Write(path)) 747 }) 748 .command("w/frames", "Write view as individual frames", |p| { 749 p.then(optional(path())) 750 .map(|(_, dir)| Command::WriteFrames(dir)) 751 }) 752 .command("e", "Edit path(s)", |p| { 753 p.then(paths()).map(|(_, paths)| Command::Edit(paths)) 754 }) 755 .command("e/frames", "Edit frames as view", |p| { 756 p.then(paths()).map(|(_, paths)| Command::EditFrames(paths)) 757 }) 758 .command("help", "Display help", |p| { 759 p.value(Command::Mode(Mode::Help)) 760 }) 761 .command("set", "Set setting to value", |p| { 762 p.then(setting()) 763 .skip(optional(whitespace())) 764 .then(optional( 765 symbol('=') 766 .skip(optional(whitespace())) 767 .then(Value::parser()) 768 .map(|(_, v)| v), 769 )) 770 .map(|((_, k), v)| Command::Set(k, v.unwrap_or(Value::Bool(true)))) 771 }) 772 .command("unset", "Set setting to `off`", |p| { 773 p.then(setting()) 774 .map(|(_, k)| Command::Set(k, Value::Bool(false))) 775 }) 776 .command("toggle", "Toggle setting", |p| { 777 p.then(setting()).map(|(_, k)| Command::Toggle(k)) 778 }) 779 .command("echo", "Echo setting or value", |p| { 780 p.then(Value::parser()).map(|(_, v)| Command::Echo(v)) 781 }) 782 .command("slice", "Slice view into <n> frames", |p| { 783 p.then(optional(natural::<usize>().label("<n>"))) 784 .map(|(_, n)| Command::Slice(n)) 785 }) 786 .command( 787 "source", 788 "Source an rx script (eg. palette or config)", 789 |p| p.then(optional(path())).map(|(_, p)| Command::Source(p)), 790 ) 791 .command("cd", "Change current directory", |p| { 792 p.then(optional(path())).map(|(_, p)| Command::ChangeDir(p)) 793 }) 794 .command("zoom", "Zoom view", |p| { 795 p.then( 796 peek(rational::<f32>().label("<level>")) 797 .try_map(|z| { 798 if z >= 1.0 { 799 Ok(Command::Zoom(Op::Set(z))) 800 } else { 801 Err("zoom level must be >= 1.0") 802 } 803 }) 804 .or(symbol('+') 805 .value(Command::Zoom(Op::Incr)) 806 .or(symbol('-').value(Command::Zoom(Op::Decr))) 807 .or(fail("couldn't parse zoom parameter"))) 808 .label("+/-"), 809 ) 810 .map(|(_, cmd)| cmd) 811 }) 812 .command("brush/size", "Set brush size", |p| { 813 p.then( 814 natural::<usize>() 815 .label("<size>") 816 .map(|z| Command::BrushSize(Op::Set(z as f32))) 817 .or(symbol('+') 818 .value(Command::BrushSize(Op::Incr)) 819 .or(symbol('-').value(Command::BrushSize(Op::Decr))) 820 .or(fail("couldn't parse brush size parameter"))) 821 .label("+/-"), 822 ) 823 .map(|(_, cmd)| cmd) 824 }) 825 .command( 826 "brush/set", 827 "Set brush mode, eg. `xsym` for x-symmetry", 828 |p| { 829 p.then(param::<BrushMode>()) 830 .map(|(_, m)| Command::BrushSet(m)) 831 }, 832 ) 833 .command("brush/unset", "Unset brush mode", |p| { 834 p.then(param::<BrushMode>()) 835 .map(|(_, m)| Command::BrushUnset(m)) 836 }) 837 .command("brush/toggle", "Toggle brush mode", |p| { 838 p.then(param::<BrushMode>()) 839 .map(|(_, m)| Command::BrushToggle(m)) 840 }) 841 .command("brush", "Switch to brush", |p| { 842 p.value(Command::Tool(Tool::Brush)) 843 }) 844 .command("flood", "Switch to flood fill tool", |p| { 845 p.value(Command::Tool(Tool::FloodFill)) 846 }) 847 .command("mode", "Set session mode, eg. `visual` or `normal`", |p| { 848 p.then(param::<Mode>()).map(|(_, m)| Command::Mode(m)) 849 }) 850 .command("visual", "Set session mode to visual", |p| { 851 p.map(|_| Command::Mode(Mode::Visual(VisualState::default()))) 852 }) 853 .command("sampler/off", "Switch the sampler tool off", |p| { 854 p.value(Command::ToolPrev) 855 }) 856 .command("sampler", "Switch to the sampler tool", |p| { 857 p.value(Command::Tool(Tool::Sampler)) 858 }) 859 .command("v/next", "Activate the next view", |p| { 860 p.value(Command::ViewNext) 861 }) 862 .command("v/prev", "Activate the previous view", |p| { 863 p.value(Command::ViewPrev) 864 }) 865 .command("v/center", "Center the active view", |p| { 866 p.value(Command::ViewCenter) 867 }) 868 .command("v/clear", "Clear the active view", |p| { 869 p.value(Command::Fill(Some(Rgba8::TRANSPARENT))) 870 }) 871 .command("v/fill", "Fill the active view", |p| { 872 p.then(optional(color())).map(|(_, c)| Command::Fill(c)) 873 }) 874 .command("pan", "Switch to the pan tool", |p| { 875 p.then(tuple::<i32>(integer().label("<x>"), integer().label("<y>"))) 876 .map(|(_, (x, y))| Command::Pan(x, y)) 877 }) 878 .command("map", "Map keys to a command in all modes", |p| { 879 p.then(KeyMapping::parser(&[ 880 Mode::Normal, 881 Mode::Visual(VisualState::selecting()), 882 Mode::Visual(VisualState::Pasting), 883 ])) 884 .map(|(_, km)| Command::Map(Box::new(km))) 885 }) 886 .command("map/visual", "Map keys to a command in visual mode", |p| { 887 p.then(KeyMapping::parser(&[ 888 Mode::Visual(VisualState::selecting()), 889 Mode::Visual(VisualState::Pasting), 890 ])) 891 .map(|(_, km)| Command::Map(Box::new(km))) 892 }) 893 .command("map/normal", "Map keys to a command in normal mode", |p| { 894 p.then(KeyMapping::parser(&[Mode::Normal])) 895 .map(|(_, km)| Command::Map(Box::new(km))) 896 }) 897 .command("map/help", "Map keys to a command in help mode", |p| { 898 p.then(KeyMapping::parser(&[Mode::Help])) 899 .map(|(_, km)| Command::Map(Box::new(km))) 900 }) 901 .command("map/clear!", "Clear all key mappings", |p| { 902 p.value(Command::MapClear) 903 }) 904 .command("p/add", "Add a color to the palette", |p| { 905 p.then(color()).map(|(_, rgba)| Command::PaletteAdd(rgba)) 906 }) 907 .command("p/clear", "Clear the color palette", |p| { 908 p.value(Command::PaletteClear) 909 }) 910 .command("p/gradient", "Add a gradient to the palette", |p| { 911 p.then(tuple::<Rgba8>( 912 color().label("<from>"), 913 color().label("<to>"), 914 )) 915 .skip(whitespace()) 916 .then(natural::<usize>().label("<count>")) 917 .map(|((_, (cs, ce)), n)| Command::PaletteGradient(cs, ce, n)) 918 }) 919 .command( 920 "p/sample", 921 "Sample palette colors from the active view", 922 |p| p.value(Command::PaletteSample), 923 ) 924 .command("p/sort", "Sort the palette colors", |p| { 925 p.value(Command::PaletteSort) 926 }) 927 .command("p/write", "Write the color palette to a file", |p| { 928 p.then(path()).map(|(_, path)| Command::PaletteWrite(path)) 929 }) 930 .command("undo", "Undo the last edit", |p| p.value(Command::Undo)) 931 .command("redo", "Redo the last edit", |p| p.value(Command::Redo)) 932 .command("f/add", "Add a blank frame to the active view", |p| { 933 p.value(Command::FrameAdd) 934 }) 935 .command("f/clone", "Clone a frame and add it to the view", |p| { 936 p.then(optional(integer::<i32>().label("<index>"))) 937 .map(|(_, index)| Command::FrameClone(index.unwrap_or(-1))) 938 }) 939 .command( 940 "f/remove", 941 "Remove the last frame from the active view", 942 |p| p.value(Command::FrameRemove), 943 ) 944 .command("f/prev", "Navigate to previous frame", |p| { 945 p.value(Command::FramePrev) 946 }) 947 .command("f/next", "Navigate to next frame", |p| { 948 p.value(Command::FrameNext) 949 }) 950 .command("f/resize", "Resize the active view frame(s)", |p| { 951 p.then(tuple::<u32>( 952 natural().label("<width>"), 953 natural().label("<height>"), 954 )) 955 .map(|(_, (w, h))| Command::FrameResize(w, h)) 956 }) 957 .command("tool", "Switch tool", |p| { 958 p.then(word().label("pan/brush/sampler/..")) 959 .try_map(|(_, t)| match t.as_str() { 960 "pan" => Ok(Command::Tool(Tool::Pan(PanState::default()))), 961 "brush" => Ok(Command::Tool(Tool::Brush)), 962 "sampler" => Ok(Command::Tool(Tool::Sampler)), 963 _ => Err(format!("unknown tool {:?}", t)), 964 }) 965 }) 966 .command("tool/prev", "Switch to previous tool", |p| { 967 p.value(Command::ToolPrev) 968 }) 969 .command("swap", "Swap foreground and background colors", |p| { 970 p.value(Command::SwapColors) 971 }) 972 .command("reset!", "Reset all settings to defaults", |p| { 973 p.value(Command::Reset) 974 }) 975 .command("selection/move", "Move selection", |p| { 976 p.then(tuple::<i32>(integer().label("<x>"), integer().label("<y>"))) 977 .map(|(_, (x, y))| Command::SelectionMove(x, y)) 978 }) 979 .command("selection/resize", "Resize selection", |p| { 980 p.then(tuple::<i32>(integer().label("<x>"), integer().label("<y>"))) 981 .map(|(_, (x, y))| Command::SelectionResize(x, y)) 982 }) 983 .command("selection/yank", "Yank/copy selection content", |p| { 984 p.value(Command::SelectionYank) 985 }) 986 .command("selection/cut", "Cut selection content", |p| { 987 p.value(Command::SelectionCut) 988 }) 989 .command("selection/paste", "Paste into selection", |p| { 990 p.value(Command::SelectionPaste) 991 }) 992 .command("selection/expand", "Expand selection", |p| { 993 p.value(Command::SelectionExpand) 994 }) 995 .command("selection/erase", "Erase selection contents", |p| { 996 p.value(Command::SelectionErase) 997 }) 998 .command("selection/offset", "Offset selection bounds", |p| { 999 p.then(tuple::<i32>(integer().label("<x>"), integer().label("<y>"))) 1000 .map(|(_, (x, y))| Command::SelectionOffset(x, y)) 1001 }) 1002 .command("selection/jump", "Translate selection by one frame", |p| { 1003 p.then(param::<Direction>()) 1004 .map(|(_, dir)| Command::SelectionJump(dir)) 1005 }) 1006 .command("selection/fill", "Fill selection with color", |p| { 1007 p.then(optional(color())) 1008 .map(|(_, rgba)| Command::SelectionFill(rgba)) 1009 }) 1010 .command("selection/flip", "Flip selection", |p| { 1011 p.then(word().label("x/y")) 1012 .try_map(|(_, t)| match t.as_str() { 1013 "x" => Ok(Command::SelectionFlip(Axis::Horizontal)), 1014 "y" => Ok(Command::SelectionFlip(Axis::Vertical)), 1015 _ => Err(format!("unknown axis {:?}, must be 'x' or 'y'", t)), 1016 }) 1017 }) 1018 .command("paint/color", "Paint color", |p| { 1019 p.then(color()) 1020 .skip(whitespace()) 1021 .then(tuple::<i32>(integer().label("<x>"), integer().label("<y>"))) 1022 .map(|((_, rgba), (x, y))| Command::PaintColor(rgba, x, y)) 1023 }) 1024 .command("paint/line", "Draw a line between two points", |p| { 1025 p.then(color()) 1026 .skip(whitespace()) 1027 .then(tuple::<i32>( 1028 integer().label("<x1>"), 1029 integer().label("<y1>"), 1030 )) 1031 .skip(whitespace()) 1032 .then(tuple::<i32>( 1033 integer().label("<x2>"), 1034 integer().label("<y2>"), 1035 )) 1036 .map(|(((_, color), (x1, y1)), (x2, y2))| { 1037 Command::PaintLine(color, x1, y1, x2, y2) 1038 }) 1039 }) 1040 .command("paint/fg", "Paint foreground color", |p| { 1041 p.then(tuple::<i32>(integer().label("<x>"), integer().label("<y>"))) 1042 .map(|(_, (x, y))| Command::PaintForeground(x, y)) 1043 }) 1044 .command("paint/bg", "Paint background color", |p| { 1045 p.then(tuple::<i32>(integer().label("<x>"), integer().label("<y>"))) 1046 .map(|(_, (x, y))| Command::PaintBackground(x, y)) 1047 }) 1048 .command("paint/p", "Paint palette color", |p| { 1049 p.then(natural::<usize>()) 1050 .skip(whitespace()) 1051 .then(tuple::<i32>(integer().label("<x>"), integer().label("<y>"))) 1052 .map(|((_, i), (x, y))| Command::PaintPalette(i, x, y)) 1053 }) 1054 } 1055 } 1056 1057 /////////////////////////////////////////////////////////////////////////////// 1058 1059 #[derive(Debug)] 1060 pub struct CommandCompleter { 1061 file_completer: FileCompleter, 1062 } 1063 1064 impl CommandCompleter { 1065 fn new<P: AsRef<Path>>(cwd: P, exts: &[&str]) -> Self { 1066 Self { 1067 file_completer: FileCompleter::new(cwd, exts), 1068 } 1069 } 1070 } 1071 1072 impl autocomplete::Completer for CommandCompleter { 1073 type Options = (); 1074 1075 fn complete(&self, input: &str, _opts: ()) -> Vec<String> { 1076 let p = Commands::default().parser(); 1077 1078 match p.parse(input) { 1079 Ok((cmd, _)) => match cmd { 1080 Command::ChangeDir(path) | Command::WriteFrames(path) => self.complete_path( 1081 path.as_ref(), 1082 input, 1083 FileCompleterOpts { directories: true }, 1084 ), 1085 Command::Source(path) | Command::Write(path) => { 1086 self.complete_path(path.as_ref(), input, Default::default()) 1087 } 1088 Command::Edit(paths) | Command::EditFrames(paths) => { 1089 self.complete_path(paths.last(), input, Default::default()) 1090 } 1091 _ => vec![], 1092 }, 1093 Err(_) => vec![], 1094 } 1095 } 1096 } 1097 1098 impl CommandCompleter { 1099 fn complete_path( 1100 &self, 1101 path: Option<&String>, 1102 input: &str, 1103 opts: FileCompleterOpts, 1104 ) -> Vec<String> { 1105 use crate::autocomplete::Completer; 1106 1107 let empty = "".to_owned(); 1108 let path = path.unwrap_or(&empty); 1109 1110 // If there's whitespace between the path and the cursor, don't complete the path. 1111 // Instead, complete as if the input was empty. 1112 match input.chars().next_back() { 1113 Some(c) if c.is_whitespace() => self.file_completer.complete("", opts), 1114 _ => self.file_completer.complete(path, opts), 1115 } 1116 } 1117 } 1118 1119 #[cfg(test)] 1120 mod test { 1121 use super::*; 1122 use std::{fs, fs::File}; 1123 1124 #[test] 1125 fn test_command_completer() { 1126 let tmp = tempfile::tempdir().unwrap(); 1127 1128 for file_name in &["one.png", "two.png", "three.png"] { 1129 let path = tmp.path().join(file_name); 1130 File::create(path).unwrap(); 1131 } 1132 1133 let cc = CommandCompleter::new(tmp.path(), &["png"]); 1134 let mut auto = Autocomplete::new(cc); 1135 1136 assert_eq!(auto.next(":e |", 3), Some(("one.png".to_owned(), 3..3))); 1137 auto.invalidate(); 1138 assert_eq!( 1139 auto.next(":e |one.png", 3), 1140 Some(("one.png".to_owned(), 3..3)) 1141 ); 1142 1143 auto.invalidate(); 1144 assert_eq!( 1145 auto.next(":e one.png | two.png", 11), 1146 Some(("one.png".to_owned(), 11..11)) 1147 ); 1148 assert_eq!( 1149 auto.next(":e one.png one.png| two.png", 20), 1150 Some(("three.png".to_owned(), 11..18)) 1151 ); 1152 assert_eq!( 1153 auto.next(":e one.png three.png| two.png", 18), 1154 Some(("two.png".to_owned(), 11..20)) 1155 ); 1156 1157 fs::create_dir(tmp.path().join("assets")).unwrap(); 1158 for file_name in &["four.png", "five.png", "six.png"] { 1159 let path = tmp.path().join("assets").join(file_name); 1160 File::create(path).unwrap(); 1161 } 1162 1163 auto.invalidate(); 1164 assert_eq!( 1165 auto.next(":e assets/|", 10), 1166 Some(("five.png".to_owned(), 10..10)) 1167 ); 1168 } 1169 1170 #[test] 1171 fn test_command_line() { 1172 let tmp = tempfile::tempdir().unwrap(); 1173 1174 fs::create_dir(tmp.path().join("assets")).unwrap(); 1175 for file_name in &["one.png", "two.png", "three.png"] { 1176 let path = tmp.path().join(file_name); 1177 File::create(path).unwrap(); 1178 } 1179 for file_name in &["four.png", "five.png"] { 1180 let path = tmp.path().join("assets").join(file_name); 1181 File::create(path).unwrap(); 1182 } 1183 1184 let mut cli = CommandLine::new(tmp.path(), &tmp.path().join(".history"), &["png"]); 1185 1186 cli.puts(":e one"); 1187 cli.completion_next(); 1188 assert_eq!(cli.input(), ":e one.png"); 1189 1190 cli.completion_next(); 1191 assert_eq!(cli.input(), ":e one.png"); 1192 1193 cli.clear(); 1194 cli.puts(":e "); 1195 cli.completion_next(); 1196 assert_eq!(cli.input(), ":e assets"); 1197 1198 cli.completion_next(); 1199 assert_eq!(cli.input(), ":e one.png"); 1200 1201 cli.completion_next(); 1202 assert_eq!(cli.input(), ":e three.png"); 1203 1204 cli.completion_next(); 1205 assert_eq!(cli.input(), ":e two.png"); 1206 1207 cli.completion_next(); 1208 assert_eq!(cli.input(), ":e assets"); 1209 1210 cli.putc('/'); 1211 cli.completion_next(); 1212 assert_eq!(cli.input(), ":e assets/five.png"); 1213 1214 cli.completion_next(); 1215 assert_eq!(cli.input(), ":e assets/four.png"); 1216 1217 cli.completion_next(); 1218 assert_eq!(cli.input(), ":e assets/five.png"); 1219 1220 cli.putc(' '); 1221 cli.completion_next(); 1222 assert_eq!(cli.input(), ":e assets/five.png assets"); 1223 cli.completion_next(); 1224 assert_eq!(cli.input(), ":e assets/five.png one.png"); 1225 1226 cli.putc(' '); 1227 cli.putc('t'); 1228 cli.completion_next(); 1229 assert_eq!(cli.input(), ":e assets/five.png one.png three.png"); 1230 1231 cli.completion_next(); 1232 assert_eq!(cli.input(), ":e assets/five.png one.png two.png"); 1233 1234 cli.completion_next(); 1235 assert_eq!(cli.input(), ":e assets/five.png one.png three.png"); 1236 1237 for _ in 0..10 { 1238 cli.cursor_backward(); 1239 } 1240 cli.putc(' '); 1241 cli.putc('o'); 1242 cli.completion_next(); 1243 assert_eq!(cli.input(), ":e assets/five.png one.png one.png three.png"); 1244 1245 cli.clear(); 1246 cli.puts(":e assets"); 1247 cli.completion_next(); 1248 assert_eq!(cli.input(), ":e assets/"); 1249 1250 cli.clear(); 1251 cli.puts(":e asset"); 1252 1253 cli.completion_next(); 1254 assert_eq!(cli.input(), ":e assets/"); 1255 1256 cli.completion_next(); 1257 assert_eq!(cli.input(), ":e assets/five.png"); 1258 } 1259 1260 #[test] 1261 fn test_command_line_change_dir() { 1262 let tmp = tempfile::tempdir().unwrap(); 1263 1264 fs::create_dir(tmp.path().join("assets")).unwrap(); 1265 for file_name in &["four.png", "five.png"] { 1266 let path = tmp.path().join("assets").join(file_name); 1267 File::create(path).unwrap(); 1268 } 1269 1270 let mut cli = CommandLine::new(tmp.path(), Path::new("/dev/null"), &["png"]); 1271 1272 cli.set_cwd(tmp.path().join("assets/").as_path()); 1273 cli.puts(":e "); 1274 1275 cli.completion_next(); 1276 assert_eq!(cli.input(), ":e five.png"); 1277 1278 cli.completion_next(); 1279 assert_eq!(cli.input(), ":e four.png"); 1280 } 1281 1282 #[test] 1283 fn test_command_line_cd() { 1284 let tmp = tempfile::tempdir().unwrap(); 1285 1286 fs::create_dir(tmp.path().join("assets")).unwrap(); 1287 fs::create_dir(tmp.path().join("assets").join("1")).unwrap(); 1288 fs::create_dir(tmp.path().join("assets").join("2")).unwrap(); 1289 File::create(tmp.path().join("assets").join("rx.png")).unwrap(); 1290 1291 let mut cli = CommandLine::new(tmp.path(), Path::new("/dev/null"), &["png"]); 1292 1293 cli.clear(); 1294 cli.puts(":cd assets/"); 1295 1296 cli.completion_next(); 1297 assert_eq!(cli.input(), ":cd assets/1"); 1298 1299 cli.completion_next(); 1300 assert_eq!(cli.input(), ":cd assets/2"); 1301 1302 cli.completion_next(); 1303 assert_eq!(cli.input(), ":cd assets/1"); 1304 } 1305 1306 #[test] 1307 fn test_command_line_cursor() { 1308 let mut cli = CommandLine::new("/dev/null", "/dev/null", &[]); 1309 1310 cli.puts(":echo"); 1311 cli.delc(); 1312 assert_eq!(cli.input(), ":ech"); 1313 cli.delc(); 1314 assert_eq!(cli.input(), ":ec"); 1315 cli.delc(); 1316 assert_eq!(cli.input(), ":e"); 1317 cli.delc(); 1318 assert_eq!(cli.input(), ":"); 1319 cli.delc(); 1320 assert_eq!(cli.input(), ""); 1321 1322 cli.clear(); 1323 cli.puts(":e"); 1324 1325 assert_eq!(cli.peek(), None); 1326 cli.cursor_backward(); 1327 1328 assert_eq!(cli.peek(), Some('e')); 1329 cli.cursor_backward(); 1330 1331 assert_eq!(cli.peek(), Some('e')); 1332 assert_eq!(cli.peek_back(), Some(':')); 1333 1334 cli.delc(); 1335 assert_eq!(cli.input(), ":e"); 1336 1337 cli.clear(); 1338 cli.puts(":echo"); 1339 1340 assert_eq!(cli.peek(), None); 1341 cli.cursor_back(); 1342 1343 assert_eq!(cli.peek(), Some('e')); 1344 assert_eq!(cli.peek_back(), Some(':')); 1345 1346 cli.cursor_front(); 1347 assert_eq!(cli.peek(), None); 1348 assert_eq!(cli.peek_back(), Some('o')); 1349 } 1350 1351 #[test] 1352 fn test_parser() { 1353 let p = Commands::default().line_parser(); 1354 1355 assert_eq!( 1356 p.parse(":set foo = value"), 1357 Ok(( 1358 Command::Set("foo".to_owned(), Value::Ident(String::from("value"))), 1359 "" 1360 )) 1361 ); 1362 assert_eq!( 1363 p.parse(":set scale = 1.0"), 1364 Ok((Command::Set("scale".to_owned(), Value::F64(1.0)), "")) 1365 ); 1366 assert_eq!( 1367 p.parse(":set foo=value"), 1368 Ok(( 1369 Command::Set("foo".to_owned(), Value::Ident(String::from("value"))), 1370 "" 1371 )) 1372 ); 1373 assert_eq!( 1374 p.parse(":set foo"), 1375 Ok((Command::Set("foo".to_owned(), Value::Bool(true)), "")) 1376 ); 1377 1378 assert_eq!( 1379 param::<platform::Key>() 1380 .parse("<hello>") 1381 .unwrap_err() 1382 .0 1383 .to_string(), 1384 "unknown key <hello>" 1385 ); 1386 1387 assert_eq!(p.parse(":").unwrap(), (Command::Noop, "")); 1388 } 1389 1390 #[test] 1391 fn test_echo_command() { 1392 let p = Commands::default().line_parser(); 1393 1394 p.parse(":echo 42").unwrap(); 1395 p.parse(":echo \"hello.\"").unwrap(); 1396 p.parse(":echo \"\"").unwrap(); 1397 } 1398 1399 #[test] 1400 fn test_zoom_command() { 1401 let p = Commands::default().line_parser(); 1402 1403 assert!(p.parse(":zoom -").is_ok()); 1404 assert!(p.parse(":zoom 3.0").is_ok()); 1405 assert!(p.parse(":zoom -1.0").is_err()); 1406 } 1407 1408 #[test] 1409 fn test_vfill_commands() { 1410 let p = Commands::default().line_parser(); 1411 1412 p.parse(":v/fill").unwrap(); 1413 p.parse(":v/fill #ff00ff").unwrap(); 1414 } 1415 1416 #[test] 1417 fn test_unknown_command() { 1418 let p = Commands::default().line_parser(); 1419 1420 let (err, rest) = p.parse(":fnord").unwrap_err(); 1421 assert_eq!(rest, "fnord"); 1422 assert_eq!(err.to_string(), "unknown command: fnord"); 1423 1424 let (err, rest) = p.parse(":mode fnord").unwrap_err(); 1425 assert_eq!(rest, "fnord"); 1426 assert_eq!(err.to_string(), "unknown mode: fnord"); 1427 } 1428 1429 #[test] 1430 fn test_keymapping_parser() { 1431 let p = string("map") 1432 .skip(whitespace()) 1433 .then(KeyMapping::parser(&[])); 1434 1435 let (_, rest) = p.parse("map <tab> :q! {:q}").unwrap(); 1436 assert_eq!(rest, ""); 1437 1438 let (_, rest) = p 1439 .parse("map <tab> :brush/set erase {:brush/unset erase}") 1440 .unwrap(); 1441 assert_eq!(rest, ""); 1442 1443 let (_, rest) = p.parse("map <ctrl> :tool sampler {:tool/prev}").unwrap(); 1444 assert_eq!(rest, ""); 1445 } 1446 1447 #[test] 1448 fn tes_value_parser() { 1449 let p = Value::parser(); 1450 1451 assert_eq!(p.parse("1.0 2.0").unwrap(), (Value::F32Tuple(1.0, 2.0), "")); 1452 assert_eq!(p.parse("1.0").unwrap(), (Value::F64(1.0), "")); 1453 assert_eq!(p.parse("1").unwrap(), (Value::U32(1), "")); 1454 assert_eq!(p.parse("1 2").unwrap(), (Value::U32Tuple(1, 2), "")); 1455 assert_eq!(p.parse("on").unwrap(), (Value::Bool(true), "")); 1456 assert_eq!(p.parse("off").unwrap(), (Value::Bool(false), "")); 1457 assert_eq!( 1458 p.parse("#ff00ff").unwrap(), 1459 (Value::Rgba8(Rgba8::new(0xff, 0x0, 0xff, 0xff)), "") 1460 ); 1461 } 1462 1463 #[test] 1464 fn test_parser_errors() { 1465 let p = Commands::default().line_parser(); 1466 1467 let (err, _) = p 1468 .parse(":map <ctrl> :tool sampler {:tool/prev") 1469 .unwrap_err(); 1470 assert_eq!(err.to_string(), "unclosed '{' delimiter".to_string()); 1471 1472 let (err, _) = p.parse(":map <ctrl> :tool sampler :tool/prev").unwrap_err(); 1473 assert_eq!( 1474 err.to_string(), 1475 "extraneous input found: :tool/prev".to_string() 1476 ); 1477 } 1478 }