├── .gitignore ├── CONDUCT.md ├── COPYING ├── Cargo.toml ├── README.md ├── build_test.sh ├── docs ├── protocol.md └── screen_divisions.md ├── notty-cairo ├── Cargo.toml ├── pangocairo │ ├── Cargo.toml │ └── src │ │ ├── ffi.rs │ │ ├── lib.rs │ │ └── wrap │ │ ├── attr.rs │ │ ├── attr_list.rs │ │ └── layout.rs └── src │ ├── cfg.rs │ ├── image_renderer.rs │ ├── lib.rs │ └── text_renderer.rs ├── notty.terminfo ├── scaffolding ├── Cargo.toml ├── README.md ├── echotest │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── guernica.jpg ├── imagetest.rb ├── resources │ └── update-config.toml ├── src │ ├── cfg │ │ ├── mod.rs │ │ └── toml.rs │ ├── commands.rs │ ├── key.rs │ └── main.rs └── tty │ ├── Cargo.toml │ └── src │ └── lib.rs ├── screenshot.png └── src ├── command ├── erase.rs ├── input.rs ├── meta.rs ├── mod.rs ├── movement.rs ├── panel.rs ├── put.rs ├── respond.rs ├── style.rs └── tooltip.rs ├── datatypes ├── iter.rs ├── key.rs └── mod.rs ├── grapheme_tables.rs ├── lib.rs ├── output ├── ansi.rs ├── mod.rs └── notty │ ├── attachment.rs │ └── mod.rs └── terminal ├── char_grid ├── cell.rs ├── cursor.rs ├── grid │ └── mod.rs ├── mod.rs ├── tooltip.rs ├── view.rs └── writers │ ├── character.rs │ ├── image.rs │ ├── mod.rs │ └── tests.rs ├── input ├── ansi.rs ├── buffer.rs ├── line_echo.rs ├── mod.rs ├── modifiers.rs ├── notty.rs └── screen_echo.rs ├── interfaces.rs ├── mod.rs ├── screen ├── iter.rs ├── mod.rs ├── panel.rs ├── ring.rs ├── section.rs ├── split.rs └── tests.rs └── styles.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | scaffolding/target 3 | scaffolding/pangocairo/target 4 | scaffolding/tty/target 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # notty Code of Conduct 2 | 3 | As a Rust community project, notty is committed to upholding the 4 | [Rust Code of Conduct][rust-coc]. If in a setting associated with notty anyone 5 | does anything which you believe violates this code of conduct, or which makes 6 | you feel uncomfortable or unwelcome, please contact woboats@gmail.com and I 7 | will take action that I feel is appropriate. 8 | 9 | [rust-coc]: https://www.rust-lang.org/conduct.html 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Without Boats "] 3 | license = "AGPL-3.0+" 4 | name = "notty" 5 | version = "0.1.0" 6 | workspace = "./scaffolding" 7 | 8 | [dependencies] 9 | base64 = "0.1.1" 10 | mime = "0.1.3" 11 | unicode-width = "0.1.3" 12 | uuid = "0.3.1" 13 | 14 | [dependencies.notty-encoding] 15 | git = "https://github.com/withoutboats/notty-encoding" 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # notty - not a typewriter 2 | 3 | [![Join the chat at https://gitter.im/withoutboats/notty](https://badges.gitter.im/withoutboats/notty.svg)](https://gitter.im/withoutboats/notty?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | ![](screenshot.png) 6 | 7 | __notty__ is a virtual terminal like xterm, gnome-vte, sh, or rxvt. Unlike 8 | these programs, __notty__ is not intended to emulate a DEC VT-series physical 9 | video terminal, or any other physical device. Instead, __notty__ is an 10 | experimental project to bring new features to the command-line which would not 11 | have been possible for the physical terminals other terminals emulate. 12 | 13 | Current command-line tools have stagnated (if you prefer, 'stabilized') around 14 | the ECMA-48/ISO 6429/ANSI X3.64 escape code protocol, which is defined on the 15 | basis of the capabilities of 1980s video terminal devices. Essentially all 16 | terminals in use today are virtual terminals being run on devices which are 17 | significantly more capable than these ancient machines, and yet the terminal 18 | environment has not kept pace with these developments. The last revision of the 19 | ANSI escape code protocol was released in 1991. 20 | 21 | __notty__ will attempt to remain true to the text-oriented, command-line user 22 | interface of the terminal while extending it to include new and more powerful 23 | interface features, such as: 24 | 25 | * Full support for rich text formatting, including 24-bits of color. 26 | * Full and correct support for all of Unicode. 27 | * Lossless keyboard input. 28 | * Inline media content, including raster graphics and structured data. 29 | * Dropdown menus, tooltips, and other features which do not strictly reside in 30 | the character grid. 31 | * Local echo and retained off-screen character grid state to reduce the need 32 | for the tty to transmit data back to the terminal. 33 | * Subdividing the character grid to enable more complex interface layouts 34 | without repeatedly reimplementing that logic in the controlling process. 35 | * And more! If you know any features you wish the terminal had, please open an 36 | issue and let's talk about this. 37 | 38 | Many of these features are not yet implemented. 39 | 40 | To achieve these ends, __notty__ will implement a new and more consistent escape 41 | protocol than ANSI escape codes. This protocol will be comparatively easy to 42 | extend with new features as the project grows. Once a feature set has been 43 | stabilized, I will write a framework for creating terminal applications that 44 | use __notty__'s features, written against this protocol (allowing other 45 | terminals to implement the protocol and support these features as well). This 46 | framework will include a polyfill for approximating these features as well as 47 | possible in terminals that don't implement notty codes. 48 | 49 | This repository is a library which defines an engine for translating both ANSI 50 | and __notty__ escape codes into state changes in the terminal's abstract state. 51 | This library does not directly implement any means of drawing the state to the 52 | screen and is agnostic about the interface it uses to communicate with the 53 | controlling process, so hopefully it will be reusable for writing terminals in 54 | different graphical environments, for writing screen/tmux-like server-side 55 | multi-terminal managers, and for writing SSH clients in non-UNIX environments. 56 | Because it implements (most) ANSI escape codes, this terminal is backwards 57 | compatible with existing command line tools. 58 | 59 | A subdirectory, named scaffolding, contains a minimal graphical terminal using 60 | GTK/pango/cairo, intended for testing __notty__'s features interactively. This 61 | terminal is buggy and feature poor and not intended for general use. 62 | 63 | A major difference between __notty__ and other projects in the same space is 64 | that this is _just_ a virtual terminal, and is fully backwards compatible with 65 | the existing shell/terminal setup. It does not implement any features of a 66 | shell, and it is not attempting to totally supplant any existing paradigms. 67 | Graphical terminals based on this library should be useable as drop-in 68 | replacements for other terminals, but with new features that can be used to 69 | implement better interfaces for command line programs such as shells, text 70 | editors, and other utilities. 71 | 72 | That said, terminals as they exist today are a pile of ugly kludges. From the 73 | kernel's tty/pty subsystem, to the termios ioctl calls which control it, to the 74 | terminfo and termcap databases, to the ANSI escape codes they describe, to the 75 | ancient codebases of the terminal emulators themselves, this is a universe of 76 | arcane and poorly documented old growth code, much of which is no longer 77 | actively useful to people in the 21st century - your system ships with a terminfo db page 78 | for more than 2500 different terminal devices, nearly all of them extinct, and 79 | every new console you open has a baud rate set in the kernel, even though it 80 | exchanges data in memory. More advanced features of __notty__ will certainly 81 | requiring sidestepping this system to a certain extent: the current plan is to 82 | implement a command to "switch" notty to an extended mode; in such a mode, only 83 | notty escape codes would be used and the tty's flags would all be unset except 84 | for CREAD and ISIG (and maybe not even ISIG). 85 | 86 | This implementation is written in [Rust][rust], an exciting new systems 87 | language from Mozilla. 88 | 89 | Much thanks to __Thomas E. Dickey__, the maintainer of xterm, 90 | [whose website][invis-island] hosts excellent documentation regarding xterm's 91 | behavior, and to __Paul Flo Williams__, who maintains [vt100.net][vt100], 92 | which hosts manuals for the actual DEC VT-series terminals. Credit also to 93 | __Gary Bernhardt__, whose talk [A Whole New World][anterminal] influenced me to 94 | pursue this project seriously. 95 | 96 | # License 97 | 98 | __notty__ is free software: you can redistribute it and/or modify it under the 99 | terms of the __GNU Affero General Public License__ as published by the Free 100 | Software Foundation, either __version 3__ of the License, or (at your option) 101 | any later version. 102 | 103 | This program is distributed in the hope that it will be useful, but __WITHOUT 104 | ANY WARRANTY__; without even the implied warranty of __MERCHANTABILITY__ or 105 | __FITNESS FOR A PARTICULAR PURPOSE__. See the GNU Affero General Public 106 | License for more details. 107 | 108 | You should have received a copy of the GNU Affero General Public License 109 | along with this program. If not, see . 110 | 111 | # Conduct 112 | 113 | The __notty__ project is committed to upholding the 114 | [Rust Code of Conduct][rust-coc]. Please see CONDUCT.md for more information. 115 | 116 | [anterminal]: https://destroyallsoftware.com/talks/a-whole-new-world 117 | [invis-island]: http://invisible-island.net/xterm 118 | [rust]: https://www.rust-lang.org 119 | [rust-coc]: https://www.rust-lang.org/conduct.html 120 | [vt100]: http://vt100.net 121 | -------------------------------------------------------------------------------- /build_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This script tests both the natty and scaffolding crates, to make sure that 3 | # changes to natty's public API do not break the scaffolding terminal 4 | 5 | test_subcrate() { 6 | cd $1 && cargo test && cd .. 7 | } 8 | 9 | cargo test && test_subcrate 'notty-cairo' && test_subcrate 'scaffolding' 10 | -------------------------------------------------------------------------------- /docs/protocol.md: -------------------------------------------------------------------------------- 1 | # Notty Escape Codes 2 | 3 | Notty escape codes are a set of escape-initiated command sequences, intended 4 | to be transmitted inline with UTF8 encoded textual data representing the input 5 | from and output to a virtual terminal program. They are analogous to so-called 6 | ANSI escape codes, defined in ECMA-48, and implement much of the same 7 | functionality. 8 | 9 | ## Structure of Notty Escape Codes 10 | 11 | All Notty escape codes have the same structure. Written in psuedo-BNF: 12 | 13 | ``` 14 | ::= "\x1b{" (";" )* ("{" )* "}" 15 | ::= 16 | ::= ("." )* 17 | ::= n @ ";" <.>{n} 18 | ::= [0-9A-Fa-f]+ 19 | ``` 20 | 21 | The sequence initializer is the string U+1b,U+7b (the escape character and the 22 | left curly brace). This sequence is fully disambuguates notty escape codes from 23 | any standardized ANSI escape codes, which do not ever begin with these 24 | charachters. 25 | 26 | The next component of the code is a hexademically encoded opcode. Every valid 27 | command is assigned an opcode, which defines what subsequent components are 28 | a validly structured commands. Escape codes beginning with unknown op codes 29 | should be entirely ignored. 30 | 31 | This is followed by two sections: the "arguments" section, which is a sequence 32 | of arguments to the op code which are encoded in a simple, consistent format, 33 | and the "attachments" section, which are arbitrary sequences of bytes. The 34 | command is terminated when the "}" character appears after the end of an 35 | attachment. 36 | 37 | ### Arguments 38 | 39 | Each opcode has a sequence of arguments which is expected to be transmitted 40 | with it. These arguments are well-typed values, and each opcode has only 41 | one valid arity of arguments it can receive (modulated by the note below about 42 | default values). 43 | 44 | An argument is encoded as a sequence of period separated hexadecimal integers; 45 | the arguments are separated from one another and from the op code by the 46 | semicolon character. 47 | 48 | Opcodes may have default arguments. If all remaining arguments for this 49 | command are the default values, these arguments can be omitted entirely from 50 | the code. If one argument is default, but subsequent arguments are not, and 51 | that argument is not a boolean value, it can be encoded as the '0' character, 52 | which is reserved for default arguments for all types except booleans. 53 | 54 | The argument types are: 55 | 56 | __TODO__ 57 | 58 | ### Attachments 59 | 60 | Some opcodes take attachments as well as or instead of arguments. Attachments 61 | have no general type, but each command expects attachments to be encoded in 62 | a format specified by the description of that command. 63 | 64 | Attachments come subsequent to any arguments, and each attachment is initiated 65 | by the '{' character. The first component of an attachment is a hex integer 66 | which is terminated by the ';' character. This integer is interpreted as a 67 | length, and the number of bytes equivalent to its length read after the ';' 68 | character are the data associated with that attachment. 69 | 70 | For example, the first attachment to the "PUT IMAGE AT" opcode is a MIME type, 71 | which determines how the data in the second attachment will be interpreted. 72 | The first attachment for a png would therefore be encoded as "{9;image/png", 73 | because "image/png" is 9 bytes long. 74 | 75 | ## Op Codes 76 | 77 | __TODO__ 78 | -------------------------------------------------------------------------------- /docs/screen_divisions.md: -------------------------------------------------------------------------------- 1 | # Subdividing the screen in notty 2 | 3 | notty supports subdividing a screen into multiple sections, each of which can 4 | contain independent grids. Each of these grids can retain off-screen state, 5 | scroll independently, be resized independently, and so on. There are several 6 | commands which manipulate this feature, but what's most important is 7 | understanding the underlying layout model that notty uses. Here are the 8 | rules that notty's model uses to make screen subdivision easier: 9 | 10 | 1. The screen is subdivided into nested, rectangular sections. 11 | 2. Each section of the screen contains a stack of panels. 12 | 3. Each panel is either a single character grid, or is split into two smaller 13 | sections, each of which contain a stack of panels. 14 | 15 | # For example 16 | 17 | Consider this 6x6 screen: 18 | 19 | ``` 20 | 0 1 2 3 4 5 21 | +-----+-----+ 22 | 0| | | 23 | 1| | | 24 | 2| | | 25 | 3| | | 26 | +-----+-----+ 27 | 4| | 28 | 5| | 29 | +-----+-----+ 30 | ``` 31 | 32 | Though this screen contains only 3 grids, it actually contains 5 sections: 33 | 34 | 1. The base section, which contains the entire 6x6 grid. The panel in this 35 | section is split horizontally between rows 3 and 4. 36 | 2. The top portion of the base section, filling the area from 0,0 to 5,3 37 | (inclusive). The panel in this section is split vertically between columns 2 38 | and 3. 39 | 3. The left portion of the previous section, from 0,0 to 2,3 (inclusive). The 40 | panel here contains a character grid. 41 | 4. The right portion of that section, from 3,0 to 5,3 (inclusive). The panel 42 | here contains a character grid as well. 43 | 5. The lower portion of the base section, from 0,4 to 5,5 (inclusive). The 44 | panel here also contains a character grid. 45 | 46 | # Commands that can be applied to any section 47 | 48 | In the implementation, each of these sections of the screen has an identifying 49 | tag, so that different commands can be applied to each section. These actions 50 | can all be performed on any section of the screen, whether it contains a grid 51 | or a split: 52 | 53 | ## Pushing a Panel 54 | 55 | Sections don't just contain one Panel, they contain a stack of Panels. You can 56 | push a new Panel, which contains an empty grid, over any section on the screen. 57 | 58 | This includes sections which contain split panels, and also includes the 'base 59 | section' of the entire screen. This means, for example, a new grid could be 60 | pushed over the entire screen in the above example, or over the top half of 61 | the screen that contains the vertical split. 62 | 63 | ## Popping a Panel 64 | 65 | The top panel of a section can be popped off, deleting whatever it contained. 66 | This is only true if a section contains more than one panel - a section with a 67 | stack with only one member will not change when the pop command is applied. 68 | 69 | The tag of each section is assigned when that section is created, but the tag 70 | of the base section is always 0. If two sections are given the same tag, the 71 | behavior of notty commands applied to that tag is implementation defined, and 72 | you should not create sections with the same tag. 73 | 74 | ## Rotating the Panel Stack 75 | 76 | The stack in each section can also be rotated up and down, switching which 77 | panel is on top (and therefore visible) without deleting any panels. The stack 78 | cannot be arbitrarily reordered, it can only be rotated in either direction. 79 | 80 | ## Splitting a Section 81 | 82 | Any section can be split into two sections, the split command takes arguments 83 | which handle how it should be split, including which side of the split the 84 | current content of the top panel should be saved to. 85 | 86 | Splits can be either horizontal or vertical, and can occur between any two 87 | columns/rows within that section. 88 | 89 | Only the top panel of a section is split, if that panel is popped off, whatever 90 | was underneath it will unchanged by the split. 91 | 92 | A panel containing a split can be split: the current split will be saved to 93 | one of the subsections created. This includes the base panel of the entire 94 | screen. 95 | 96 | # Commands that can be applied to split sections 97 | 98 | These commands can be applied to sections which are split. Applying them to 99 | sections which are not split produces no change. 100 | 101 | ## Unsplitting a Section 102 | 103 | A split section can be unsplit. The unsplit command takes an argument 104 | identifying which half of the split should be saved. The stack from the saved 105 | section will be pushed on top of the stack in the section that is being 106 | unsplit. 107 | 108 | ## Adjusting a Section Split 109 | 110 | The division within a split section can be adjusted, changing how that section 111 | is split. This adjustment can change both the position and axis of the split, 112 | so that a horizontal split could become a vertical split, for example. The 113 | contents of the two subsections of the split will be resized to fit the new 114 | areas of the subsections. 115 | 116 | # The 'active' section 117 | 118 | At any given time, exactly one section of the screen is marked the active 119 | section. This top panel of this section must be a character grid. 120 | 121 | All commands which apply to character grids - writing characters, setting 122 | styles, moving the cursor, and so on - are applied to the grid which is 123 | currently active. 124 | 125 | A command exists to switch which section is active at a given time. An attempt 126 | to switch the active section to a section which does not have a character grid 127 | as its top panel will result in no change. 128 | -------------------------------------------------------------------------------- /notty-cairo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Without Boats "] 3 | name = "notty-cairo" 4 | version = "0.1.0" 5 | workspace = "../scaffolding" 6 | 7 | [dependencies] 8 | cairo-rs = "0.1.0" 9 | gdk = "0.5.0" 10 | gdk-pixbuf = "0.1.0" 11 | gdk-pixbuf-sys = "0.3.1" 12 | glib = "0.1.0" 13 | gio-sys = "0.3.1" 14 | itertools = "0.5.2" 15 | libc = "0.2.11" 16 | 17 | [dependencies.notty] 18 | path = ".." 19 | 20 | [dependencies.pangocairo] 21 | path = "pangocairo" 22 | -------------------------------------------------------------------------------- /notty-cairo/pangocairo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pangocairo" 3 | version = "0.1.0" 4 | authors = ["Without Boats "] 5 | license = "AGPL-3.0+" 6 | workspace = "../../scaffolding" 7 | 8 | [dependencies] 9 | cairo-sys-rs = "0.3.0" 10 | gobject-sys = "0.3.0" 11 | pango = "0.0.7" 12 | pango-sys = "0.3.0" 13 | -------------------------------------------------------------------------------- /notty-cairo/pangocairo/src/ffi.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use cairo; 17 | use pango_sys as pango; 18 | 19 | #[link(name = "pangocairo-1.0")] 20 | extern { 21 | pub fn pango_cairo_create_layout(cr: *mut cairo::cairo_t) -> *mut pango::PangoLayout; 22 | pub fn pango_cairo_show_layout(cr: *mut cairo::cairo_t, layout: *mut pango::PangoLayout); 23 | } 24 | -------------------------------------------------------------------------------- /notty-cairo/pangocairo/src/lib.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | extern crate gobject_sys; 17 | extern crate pango; 18 | extern crate pango_sys; 19 | extern crate cairo_sys as cairo; 20 | 21 | pub mod ffi; 22 | 23 | pub mod wrap { 24 | mod attr; 25 | mod attr_list; 26 | mod layout; 27 | 28 | pub use self::attr::PangoAttribute; 29 | pub use self::attr_list::PangoAttrList; 30 | pub use self::layout::PangoLayout; 31 | } 32 | -------------------------------------------------------------------------------- /notty-cairo/pangocairo/src/wrap/attr.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::ops::Range; 17 | 18 | use pango; 19 | use pango_sys as ffi; 20 | 21 | pub struct PangoAttribute(*mut ffi::PangoAttribute); 22 | 23 | impl PangoAttribute { 24 | 25 | pub fn fg_color(range: &Range, (r,g,b): (u8, u8, u8)) -> PangoAttribute { 26 | let (r, g, b) = ((r as u16) << 8, (g as u16) << 8, (b as u16) << 8); 27 | let attr = unsafe { ffi::pango_attr_foreground_new(r, g, b) }; 28 | PangoAttribute(attr).with_range(range) 29 | } 30 | 31 | pub fn bg_color(range: &Range, (r,g,b): (u8, u8, u8)) -> PangoAttribute { 32 | let (r, g, b) = ((r as u16) << 8, (g as u16) << 8, (b as u16) << 8); 33 | let attr = unsafe { ffi::pango_attr_background_new(r, g, b) }; 34 | PangoAttribute(attr).with_range(range) 35 | } 36 | 37 | pub fn underline(range: &Range) -> PangoAttribute { 38 | let attr = unsafe { ffi::pango_attr_underline_new(ffi::PangoUnderline::Single) }; 39 | PangoAttribute(attr).with_range(range) 40 | } 41 | 42 | pub fn double_underline(range: &Range) -> PangoAttribute { 43 | let attr = unsafe { ffi::pango_attr_underline_new(ffi::PangoUnderline::Double) }; 44 | PangoAttribute(attr).with_range(range) 45 | } 46 | 47 | pub fn bold(range: &Range) -> PangoAttribute { 48 | let attr = unsafe { ffi::pango_attr_weight_new(pango::Weight::Bold) }; 49 | PangoAttribute(attr).with_range(range) 50 | } 51 | 52 | pub fn italic(range: &Range) -> PangoAttribute { 53 | let attr = unsafe { ffi::pango_attr_style_new(pango::Style::Italic) }; 54 | PangoAttribute(attr).with_range(range) 55 | } 56 | 57 | pub fn strikethrough(range: &Range) -> PangoAttribute { 58 | let attr = unsafe { ffi::pango_attr_strikethrough_new(!0) }; 59 | PangoAttribute(attr).with_range(range) 60 | } 61 | 62 | fn with_range(self, range: &Range) -> PangoAttribute { 63 | unsafe { 64 | (*self.0).start_index = range.start as u32; 65 | (*self.0).end_index = range.end as u32; 66 | } 67 | self 68 | } 69 | 70 | pub unsafe fn raw(&self) -> *mut ffi::PangoAttribute { 71 | self.0 72 | } 73 | 74 | } 75 | 76 | impl Drop for PangoAttribute { 77 | fn drop(&mut self) { 78 | unsafe { ffi::pango_attribute_destroy(self.0); } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /notty-cairo/pangocairo/src/wrap/attr_list.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::iter::FromIterator; 17 | use std::mem; 18 | 19 | use pango_sys as ffi; 20 | 21 | use super::PangoAttribute; 22 | 23 | pub struct PangoAttrList(*mut ffi::PangoAttrList); 24 | 25 | impl PangoAttrList { 26 | pub fn new() -> PangoAttrList { 27 | PangoAttrList(unsafe { ffi::pango_attr_list_new() }) 28 | } 29 | 30 | pub fn push(&self, attribute: PangoAttribute) { 31 | unsafe { 32 | ffi::pango_attr_list_insert(self.0, attribute.raw()); 33 | } 34 | mem::forget(attribute); 35 | } 36 | 37 | pub unsafe fn raw(&self) -> *mut ffi::PangoAttrList { 38 | self.0 39 | } 40 | } 41 | 42 | impl Clone for PangoAttrList { 43 | fn clone(&self) -> PangoAttrList { 44 | PangoAttrList(unsafe { ffi::pango_attr_list_ref(self.0) }) 45 | } 46 | } 47 | 48 | impl Drop for PangoAttrList { 49 | fn drop(&mut self) { 50 | unsafe { ffi::pango_attr_list_unref(self.0); } 51 | } 52 | } 53 | 54 | impl FromIterator for PangoAttrList { 55 | fn from_iter>(iterator: T) -> PangoAttrList { 56 | let list = PangoAttrList::new(); 57 | for attr in iterator { 58 | list.push(attr); 59 | } 60 | list 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /notty-cairo/pangocairo/src/wrap/layout.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::ffi::CString; 17 | use std::ptr; 18 | 19 | use cairo; 20 | use gobject_sys; 21 | use pango_sys as pango; 22 | 23 | use ffi as pangocairo; 24 | 25 | use super::PangoAttrList; 26 | 27 | pub struct PangoLayout(*mut pango::PangoLayout); 28 | 29 | impl PangoLayout { 30 | 31 | pub fn new(cairo: *mut cairo::cairo_t, 32 | font: &str, 33 | text: &str, 34 | attributes: PangoAttrList) -> PangoLayout { 35 | let font = CString::new(font.as_bytes()).unwrap().as_ptr(); 36 | unsafe { 37 | let layout = pangocairo::pango_cairo_create_layout(cairo); 38 | let font = pango::pango_font_description_from_string(font); 39 | pango::pango_layout_set_font_description(layout, font); 40 | pango::pango_font_description_free(font); 41 | pango::pango_layout_set_text(layout, 42 | text.as_bytes().as_ptr() as *const i8, 43 | text.len() as i32); 44 | pango::pango_layout_set_attributes(layout, attributes.raw()); 45 | PangoLayout(layout) 46 | } 47 | } 48 | 49 | pub fn show(&self, cairo: *mut cairo::cairo_t) { 50 | unsafe { 51 | pangocairo::pango_cairo_show_layout(cairo, self.raw()); 52 | } 53 | } 54 | 55 | pub fn extents(&self) -> (i32, i32) { 56 | let mut rec = pango::PangoRectangle { x: 0, y: 0, width: 0, height: 0 }; 57 | unsafe { 58 | pango::pango_layout_get_pixel_extents(self.raw(), ptr::null_mut(), &mut rec as *mut _); 59 | } 60 | (rec.width, rec.height) 61 | } 62 | 63 | pub unsafe fn raw(&self) -> *mut pango::PangoLayout { 64 | self.0 65 | } 66 | 67 | } 68 | 69 | impl Clone for PangoLayout { 70 | fn clone(&self) -> PangoLayout { 71 | unsafe { PangoLayout(gobject_sys::g_object_ref(self.0 as *mut _) as *mut _) } 72 | } 73 | } 74 | 75 | impl Drop for PangoLayout { 76 | fn drop(&mut self) { 77 | unsafe { gobject_sys::g_object_unref(self.0 as *mut _); } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /notty-cairo/src/image_renderer.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | use std::ptr; 3 | 4 | use cairo; 5 | 6 | use gdk::prelude::ContextExt; 7 | use glib::translate::FromGlibPtr; 8 | use notty::datatypes::MediaPosition; 9 | use notty::datatypes::MediaPosition::*; 10 | use pixbuf::{InterpType, Pixbuf}; 11 | use gio; 12 | use pixbuf_sys; 13 | use libc; 14 | 15 | use notty::terminal::Styles; 16 | 17 | pub struct ImageRenderer { 18 | pixbuf: Pixbuf, 19 | x_pos: f64, 20 | y_pos: f64, 21 | } 22 | 23 | impl ImageRenderer { 24 | pub fn new(data: &[u8], x: f64, y: f64, w: f64, h: f64, pos: MediaPosition) -> ImageRenderer { 25 | fn pixbuf_from_data(data: &[u8]) -> Option { 26 | let null = ptr::null_mut(); 27 | unsafe { 28 | let (data, len) = (mem::transmute(data.as_ptr()), data.len() as libc::ssize_t); 29 | let stream = gio::g_memory_input_stream_new_from_data(data, len, None); 30 | let pixbuf = pixbuf_sys::gdk_pixbuf_new_from_stream(stream, null, null as *mut _); 31 | if pixbuf != null as *mut _ { Some(Pixbuf::from_glib_full(pixbuf)) } 32 | else { None } 33 | } 34 | } 35 | fn empty_pixbuf() -> Pixbuf { 36 | unsafe { Pixbuf::new(0, false, 0, 1, 1).expect("Could not create empty Pixbuf.") } 37 | } 38 | 39 | let image = pixbuf_from_data(data).and_then(|img| { 40 | match pos { 41 | Display(x_align, y_align) => unimplemented!(), 42 | Fill => unimplemented!(), 43 | Fit => unimplemented!(), 44 | Stretch => { 45 | img.scale_simple(w as i32, h as i32, InterpType::Bilinear).ok() 46 | } 47 | Tile => unimplemented!(), 48 | } 49 | }).unwrap_or_else(empty_pixbuf); 50 | 51 | ImageRenderer { 52 | pixbuf: image, 53 | x_pos: x, 54 | y_pos: y, 55 | } 56 | } 57 | 58 | pub fn draw(&self, canvas: &cairo::Context) { 59 | canvas.set_source_pixbuf(&self.pixbuf, self.x_pos, self.y_pos); 60 | canvas.paint(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /notty-cairo/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(arc_counts)] 2 | extern crate cairo; 3 | extern crate gdk; 4 | extern crate gdk_pixbuf as pixbuf; 5 | extern crate gdk_pixbuf_sys as pixbuf_sys; 6 | extern crate gio_sys as gio; 7 | extern crate glib; 8 | extern crate itertools; 9 | extern crate libc; 10 | extern crate notty; 11 | extern crate pangocairo; 12 | 13 | mod cfg; 14 | mod image_renderer; 15 | mod text_renderer; 16 | 17 | use std::collections::HashMap; 18 | use std::sync::Arc; 19 | 20 | use glib::translate::ToGlibPtr; 21 | 22 | use itertools::Itertools; 23 | 24 | use notty::datatypes::Coords; 25 | use notty::terminal::{CellData, Terminal, ImageData, Styleable}; 26 | 27 | use pangocairo::wrap::{PangoLayout, PangoAttrList}; 28 | 29 | use self::cfg::gtk_color; 30 | use self::image_renderer::ImageRenderer; 31 | use self::text_renderer::TextRenderer; 32 | 33 | pub use self::cfg::{Config, TrueColor, PALETTE}; 34 | 35 | pub struct Renderer { 36 | images: HashMap, ImageRenderer>, 37 | char_d: Option<(f64, f64)>, 38 | cfg: Config, 39 | } 40 | 41 | impl Renderer { 42 | pub fn new(cfg: Config) -> Renderer { 43 | Renderer { 44 | images: HashMap::new(), 45 | char_d: None, 46 | cfg: cfg, 47 | } 48 | } 49 | 50 | pub fn reset_dimensions(&mut self, canvas: &cairo::Context, terminal: &mut Terminal, 51 | pix_w: u32, pix_h: u32) { 52 | let (char_w, char_h) = self.char_d.unwrap_or_else(|| { 53 | let char_d = self.char_dimensions(canvas); 54 | self.char_d = Some(char_d); 55 | char_d 56 | }); 57 | let width = pix_w / (char_w as u32); 58 | let height = pix_h / (char_h as u32); 59 | terminal.set_winsize(Some(width), Some(height)).unwrap_or_else(|e| panic!("{}", e)); 60 | } 61 | 62 | pub fn draw(&mut self, terminal: &Terminal, canvas: &cairo::Context) { 63 | 64 | if self.char_d.is_none() { self.char_d = Some(self.char_dimensions(canvas)); } 65 | let (r, g, b) = gtk_color(self.cfg.bg_color); 66 | canvas.set_source_rgb(r, g, b); 67 | canvas.paint(); 68 | 69 | let col_n = terminal.area().width() as usize; 70 | let rows = terminal.cells().chunks(col_n); 71 | 72 | // Remove dead images from the cache. 73 | for key in self.images.keys().filter(|k| Arc::strong_count(k) == 1).cloned().collect::>() { 74 | self.images.remove(&key); 75 | } 76 | 77 | for (y_pos, row) in rows.into_iter().enumerate() { 78 | let y_pix = self.y_pixels(y_pos as u32); 79 | let mut text = TextRenderer::new(&self.cfg, 0.0, y_pix); 80 | for (x_pos, cell) in row.enumerate() { 81 | let style = *cell.styles(); 82 | if (Coords { x: x_pos as u32, y: y_pos as u32 } == terminal.cursor().position()) { 83 | let cursor_style = *terminal.cursor().styles(); 84 | match *cell.content() { 85 | CellData::Empty => text.push_cursor(' ', style, cursor_style), 86 | CellData::Char(ch) => text.push_cursor(ch, style, cursor_style), 87 | CellData::Grapheme(ref s) => text.push_str_cursor(s, style, cursor_style), 88 | CellData::Extension(_) => unreachable!(), 89 | CellData::Image { .. } => continue, 90 | } 91 | continue; 92 | } 93 | match *cell.content() { 94 | CellData::Empty => text.push(' ', style), 95 | CellData::Char(ch) => text.push(ch, style), 96 | CellData::Grapheme(ref s) => text.push_str(s, style), 97 | CellData::Extension(_) => { } 98 | CellData::Image { ref data, ref pos, ref width, ref height, .. } => { 99 | let x_pix = self.x_pixels(x_pos as u32); 100 | if (x_pos + *width as usize) < col_n { 101 | text.draw(canvas); 102 | text = TextRenderer::new(&self.cfg, x_pix, y_pix); 103 | } 104 | if let Some(image) = self.images.get(data) { 105 | image.draw(canvas); 106 | continue; 107 | } 108 | let w_pix = self.x_pixels(*width); 109 | let h_pix = self.y_pixels(*height); 110 | let img = ImageRenderer::new(&data.data, x_pix, y_pix, w_pix, h_pix, 111 | *pos); 112 | img.draw(canvas); 113 | self.images.insert(data.clone(), img); 114 | } 115 | } 116 | } 117 | text.draw(canvas); 118 | } 119 | } 120 | 121 | fn char_dimensions(&self, canvas: &cairo::Context) -> (f64, f64) { 122 | //save the canvas position 123 | let (x_save, y_save) = canvas.get_current_point(); 124 | let string = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMN\ 125 | OPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; 126 | let cairo = canvas.to_glib_none(); 127 | let (w, h) = PangoLayout::new(cairo.0, &self.cfg.font, string, PangoAttrList::new()).extents(); 128 | canvas.move_to(x_save, y_save); 129 | ((w / string.len() as i32) as f64, h as f64) 130 | } 131 | 132 | fn x_pixels(&self, x: u32) -> f64 { 133 | self.char_d.unwrap().0 * (x as f64) 134 | } 135 | 136 | fn y_pixels(&self, y: u32) -> f64 { 137 | self.char_d.unwrap().1 * (y as f64) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /notty-cairo/src/text_renderer.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::ops::Range; 17 | 18 | use notty::datatypes::ConfigStyle; 19 | use notty::terminal::{Styles, UseStyles}; 20 | 21 | use cairo; 22 | use glib::translate::ToGlibPtr; 23 | 24 | use pangocairo::wrap::{PangoAttribute, PangoAttrList, PangoLayout}; 25 | 26 | use cfg::{TrueColor, Config}; 27 | 28 | type AppliedStyles = Vec<(Range, T)>; 29 | 30 | pub struct TextRenderer<'a> { 31 | cfg: &'a Config, 32 | x_pos: f64, 33 | y_pos: f64, 34 | 35 | text: String, 36 | fg_color: AppliedStyles, 37 | bg_color: AppliedStyles, 38 | opacity: AppliedStyles, 39 | underline: AppliedStyles<()>, 40 | double_underline: AppliedStyles<()>, 41 | bold: AppliedStyles<()>, 42 | italic: AppliedStyles<()>, 43 | strikethrough: AppliedStyles<()>, 44 | blink: AppliedStyles<()>, 45 | } 46 | 47 | impl<'a> TextRenderer<'a> { 48 | 49 | pub fn new(cfg: &'a Config, x: f64, y: f64) -> TextRenderer<'a> { 50 | TextRenderer { 51 | cfg: cfg, 52 | x_pos: x, 53 | y_pos: y, 54 | 55 | text: String::new(), 56 | fg_color: Vec::new(), 57 | bg_color: Vec::new(), 58 | opacity: Vec::new(), 59 | underline: Vec::new(), 60 | double_underline: Vec::new(), 61 | bold: Vec::new(), 62 | italic: Vec::new(), 63 | strikethrough: Vec::new(), 64 | blink: Vec::new(), 65 | } 66 | } 67 | 68 | pub fn push(&mut self, ch: char, styles: UseStyles) { 69 | let lower = self.text.len(); 70 | self.text.push(ch); 71 | let range = lower..self.text.len(); 72 | self.add_style(&range, styles); 73 | } 74 | 75 | pub fn push_str(&mut self, s: &str, styles: UseStyles) { 76 | let lower = self.text.len(); 77 | self.text.push_str(s); 78 | let range = lower..self.text.len(); 79 | self.add_style(&range, styles); 80 | } 81 | 82 | pub fn push_cursor(&mut self, ch: char, styles: UseStyles, cursor_styles: UseStyles) { 83 | let lower = self.text.len(); 84 | self.text.push(ch); 85 | let range = lower..self.text.len(); 86 | self.add_cursor_style(&range, styles, cursor_styles); 87 | } 88 | 89 | pub fn push_str_cursor(&mut self, s: &str, styles: UseStyles, cursor_styles: UseStyles) { 90 | let lower = self.text.len(); 91 | self.text.push_str(s); 92 | let range = lower..self.text.len(); 93 | self.add_cursor_style(&range, styles, cursor_styles); 94 | } 95 | 96 | pub fn draw(&self, canvas: &cairo::Context) { 97 | if self.is_blank() { return; } 98 | 99 | // Line positioning 100 | canvas.move_to(self.x_pos, self.y_pos); 101 | 102 | // Draw the text 103 | let cairo = canvas.to_glib_none(); 104 | PangoLayout::new(cairo.0, &self.cfg.font, &self.text, self.pango_attrs()).show(cairo.0); 105 | } 106 | 107 | fn is_blank(&self) -> bool { 108 | self.text.chars().all(char::is_whitespace) 109 | && self.bg_color.iter().all(|&(_, color)| color == self.cfg.bg_color) 110 | } 111 | 112 | fn add_style(&mut self, range: &Range, style: UseStyles) { 113 | match style { 114 | UseStyles::Custom(styles) => self.add_style_set(range, styles), 115 | UseStyles::Config(config) => { 116 | let styles = self.cfg.styles.get(&config) 117 | .or_else(|| self.cfg.styles.get(&ConfigStyle::Plain)) 118 | .map_or_else(Styles::default, |&s|s); 119 | self.add_style_set(range, styles); 120 | } 121 | } 122 | } 123 | 124 | fn add_style_set(&mut self, range: &Range, style: Styles) { 125 | let fg_color = self.cfg.fg_color(style.fg_color); 126 | let bg_color = self.cfg.bg_color(style.bg_color); 127 | if !style.inverted { 128 | append_field(range.clone(), fg_color, &mut self.fg_color); 129 | append_field(range.clone(), bg_color, &mut self.bg_color); 130 | } else { 131 | append_field(range.clone(), bg_color, &mut self.fg_color); 132 | append_field(range.clone(), fg_color, &mut self.bg_color); 133 | } 134 | append_field(range.clone(), style.opacity, &mut self.opacity); 135 | if style.underline { append_bool(range.clone(), &mut self.underline) } 136 | if style.double_underline { append_bool(range.clone(), &mut self.double_underline) } 137 | if style.bold { append_bool(range.clone(), &mut self.bold); } 138 | if style.italic { append_bool(range.clone(), &mut self.italic) } 139 | if style.strikethrough { append_bool(range.clone(), &mut self.strikethrough); } 140 | if style.blink { append_bool(range.clone(), &mut self.blink) } 141 | } 142 | 143 | fn add_cursor_style(&mut self, range: &Range, style: UseStyles, _: UseStyles) { 144 | match style { 145 | UseStyles::Config(config) => { 146 | let styles = self.cfg.styles.get(&config).map_or_else(Styles::default, 147 | |&s|s); 148 | self.add_style_set(range, Styles { inverted: !styles.inverted, ..styles }); 149 | }, 150 | UseStyles::Custom(style) => { 151 | self.add_style_set(range, Styles { inverted: !style.inverted, ..style }); 152 | } 153 | } 154 | } 155 | 156 | fn pango_attrs(&self) -> PangoAttrList { 157 | self.fg_color.iter().map(|&(ref range, (r,g,b))| { 158 | PangoAttribute::fg_color(range, (r,g,b)) 159 | }).chain(self.bg_color.iter().map(|&(ref range, (r,g,b))| { 160 | PangoAttribute::bg_color(range, (r,g,b)) 161 | })).chain(self.underline.iter().map(|&(ref r, _)| r).map(PangoAttribute::underline)) 162 | .chain(self.double_underline.iter().map(|&(ref r, _)| r).map(PangoAttribute::double_underline)) 163 | .chain(self.bold.iter().map(|&(ref r, _)| r).map(PangoAttribute::bold)) 164 | .chain(self.italic.iter().map(|&(ref r, _)| r).map(PangoAttribute::italic)) 165 | .chain(self.strikethrough.iter().map(|&(ref r, _)| r).map(PangoAttribute::strikethrough)) 166 | .collect() 167 | } 168 | 169 | } 170 | 171 | fn append_bool(range: Range, ranges: &mut AppliedStyles<()>) { 172 | if let Some(&mut (ref mut last_range, _)) = ranges.last_mut() { 173 | if last_range.end == range.start { 174 | return last_range.end = range.end; 175 | } 176 | } 177 | ranges.push((range, ())); 178 | } 179 | 180 | fn append_field(range: Range, field: T, ranges: &mut Vec<(Range, T)>) 181 | where T: PartialEq + Copy { 182 | if let Some(&mut (ref mut last_range, last_field)) = ranges.last_mut() { 183 | if last_field == field { 184 | last_range.end = range.end; 185 | return; 186 | } 187 | } 188 | ranges.push((range, field)); 189 | } 190 | -------------------------------------------------------------------------------- /notty.terminfo: -------------------------------------------------------------------------------- 1 | notty|natty|Not A Teletypewriter, 2 | am, 3 | bce, 4 | ccc, 5 | # hs, 6 | km, 7 | mc5i, 8 | mir, 9 | msgr, 10 | npc, 11 | xenl, 12 | colors#256, 13 | cols#80, 14 | it#8, 15 | lines#24, 16 | pairs#32767, 17 | acsc=++\,\,--..00``aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, 18 | bel=^G, 19 | bold=\E[1m, 20 | cbt=\E[Z, 21 | civis=\E[?25l, 22 | clear=\E[H\E[2J, 23 | cnorm=\E[?12l\E[?25h, 24 | cr=^M, 25 | # csr=\E[%i%p1%d;%p2%dr 26 | cub=\E[%p1%dD, 27 | cub1=^H, 28 | cud=\E[%p1%dB, 29 | cud1=^J, 30 | cuf=\E[%p1%dC, 31 | cuf1=\E[C, 32 | cup=\E[%i%p1%d;%p2%dH, 33 | cuu=\E[%p1%dA, 34 | cuu1=\E[A, 35 | cvvis=\E[?12;25h, 36 | dch=\E[%p1%dP, 37 | dch1=\E[P, 38 | dim=\E[2m, 39 | dl=\E[%p1%dM, 40 | dl1=\E[M, 41 | dsl=\E]2;\007, 42 | ech=\E[%p1%dX, 43 | ed=\E[J, 44 | el=\E[K, 45 | el1=\E[1K, 46 | flash=\E[?5h$<100/>\E[?5l, 47 | fsl=^G, 48 | home=\E[H, 49 | hpa=\E[%i%p1%dG, 50 | ht=^I, 51 | hts=\EH, 52 | ich=\E[%p1%d@, 53 | il=\E[%p1%dL, 54 | il1=\E[L, 55 | ind=^J, 56 | indn=\E[%p1%dS, 57 | initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\, 58 | invis=\E[8m, 59 | is2=\E[!p\E[?3;4l\E[4l\E>, 60 | kDC=\E[3;2~, 61 | kEND=\E[1;2F, 62 | kHOM=\E[1;2H, 63 | kIC=\E[2;2~, 64 | kLFT=\E[1;2D, 65 | kNXT=\E[6;2~, 66 | kPRV=\E[5;2~, 67 | kRIT=\E[1;2C, 68 | kb2=\EOE, 69 | kbs=\177, 70 | kcbt=\E[Z, 71 | kcub1=\EOD, 72 | kcud1=\EOB, 73 | kcuf1=\EOC, 74 | kcuu1=\EOA, 75 | kdch1=\E[3~, 76 | kend=\EOF, 77 | kent=\EOM, 78 | kf1=\EOP, 79 | kf10=\E[21~, 80 | kf11=\E[23~, 81 | kf12=\E[24~, 82 | kf13=\E[1;2P, 83 | kf14=\E[1;2Q, 84 | kf15=\E[1;2R, 85 | kf16=\E[1;2S, 86 | kf17=\E[15;2~, 87 | kf18=\E[17;2~, 88 | kf19=\E[18;2~, 89 | kf2=\EOQ, 90 | kf20=\E[19;2~, 91 | kf21=\E[20;2~, 92 | kf22=\E[21;2~, 93 | kf23=\E[23;2~, 94 | kf24=\E[24;2~, 95 | kf25=\E[1;5P, 96 | kf26=\E[1;5Q, 97 | kf27=\E[1;5R, 98 | kf28=\E[1;5S, 99 | kf29=\E[15;5~, 100 | kf3=\EOR, 101 | kf30=\E[17;5~, 102 | kf31=\E[18;5~, 103 | kf32=\E[19;5~, 104 | kf33=\E[20;5~, 105 | kf34=\E[21;5~, 106 | kf35=\E[23;5~, 107 | kf36=\E[24;5~, 108 | kf37=\E[1;6P, 109 | kf38=\E[1;6Q, 110 | kf39=\E[1;6R, 111 | kf4=\EOS, 112 | kf40=\E[1;6S, 113 | kf41=\E[15;6~, 114 | kf42=\E[17;6~, 115 | kf43=\E[18;6~, 116 | kf44=\E[19;6~, 117 | kf45=\E[20;6~, 118 | kf46=\E[21;6~, 119 | kf47=\E[23;6~, 120 | kf48=\E[24;6~, 121 | kf49=\E[1;3P, 122 | kf5=\E[15~, 123 | kf50=\E[1;3Q, 124 | kf51=\E[1;3R, 125 | kf52=\E[1;3S, 126 | kf53=\E[15;3~, 127 | kf54=\E[17;3~, 128 | kf55=\E[18;3~, 129 | kf56=\E[19;3~, 130 | kf57=\E[20;3~, 131 | kf58=\E[21;3~, 132 | kf59=\E[23;3~, 133 | kf6=\E[17~, 134 | kf60=\E[24;3~, 135 | kf61=\E[1;4P, 136 | kf62=\E[1;4Q, 137 | kf63=\E[1;4R, 138 | kf7=\E[18~, 139 | kf8=\E[19~, 140 | kf9=\E[20~, 141 | khome=\EOH, 142 | kich1=\E[2~, 143 | kind=\E[1;2B, 144 | kmous=\E[M, 145 | knp=\E[6~, 146 | kpp=\E[5~, 147 | kri=\E[1;2A, 148 | mc0=\E[i, 149 | mc4=\E[4i, 150 | mc5=\E[5i, 151 | meml=\El, 152 | memu=\Em, 153 | op=\E[39;49m, 154 | rc=\E8, 155 | rev=\E[7m, 156 | ri=\EM, 157 | rin=\E[%p1%dT, 158 | ritm=\E[23m, 159 | rmacs=\E(B, 160 | rmam=\E[?7l, 161 | rmcup=\E[?1049l, 162 | rmir=\E[4l, 163 | rmkx=\E[?1l\E>, 164 | rmm=\E[?1034l, 165 | rmso=\E[27m, 166 | rmul=\E[24m, 167 | rs1=\Ec, 168 | rs2=\E[!p\E[?3;4l\E[4l\E>, 169 | sc=\E7, 170 | setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m, 171 | setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m, 172 | sgr=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m, 173 | sgr0=\E(B\E[m, 174 | sitm=\E[3m, 175 | smacs=\E(0, 176 | smam=\E[?7h, 177 | smcup=\E[?1049h, 178 | smir=\E[4h, 179 | smkx=\E[?1h\E=, 180 | smm=\E[?1034h, 181 | smso=\E[7m, 182 | smul=\E[4m, 183 | tbc=\E[3g, 184 | tsl=\E]2;, 185 | u6=\E[%i%d;%dR, 186 | u7=\E[6n, 187 | u8=\E[?1;2c, 188 | u9=\E[c, 189 | vpa=\E[%i%p1%dd, 190 | -------------------------------------------------------------------------------- /scaffolding/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Without Boats "] 3 | license = "AGPL-3.0+" 4 | name = "scaffolding-terminal" 5 | version = "0.1.0" 6 | 7 | [dependencies] 8 | cairo-rs = "0.1" 9 | gdk = "0.5" 10 | glib = "0.1" 11 | gtk-sys = "0.3.1" 12 | toml = "0.1" 13 | 14 | [dependencies.gtk] 15 | version = "0.1" 16 | features = ["v3_16"] 17 | 18 | [dependencies.notty] 19 | path = ".." 20 | 21 | [dependencies.notty-cairo] 22 | path = "../notty-cairo" 23 | 24 | [dependencies.tty] 25 | path = "tty" 26 | 27 | [workspace] 28 | -------------------------------------------------------------------------------- /scaffolding/README.md: -------------------------------------------------------------------------------- 1 | This is a scaffolding terminal for testing the notty terminal library. It uses 2 | the notty-cairo rendering library in the same repository. 3 | 4 | __NOTE:__ This terminal is buggy, incomplete, and not intended for general use. 5 | 6 | # Building 7 | 8 | ## Rust version 9 | 10 | Currently notty requires rust nightly. Install from [rust-lang.org][rust_dl] 11 | or use [multirust][multirust]. 12 | 13 | ## Other requirements 14 | 15 | This library depends on GTK, Pango, and Cairo. [GTK-rs][gtk-rs] has install 16 | instructions for Mac OS X and Linux. 17 | 18 | [gtk-rs]: https://github.com/gtk-rs/gtk 19 | [multirust]: https://github.com/brson/multirust 20 | [rust_dl]: https://www.rust-lang.org/downloads.html 21 | -------------------------------------------------------------------------------- /scaffolding/echotest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "echotest" 3 | version = "0.1.0" 4 | authors = ["Without Boats "] 5 | 6 | [dependencies.hocus-pocus] 7 | git = "https://github.com/withoutboats/hocus-pocus" 8 | -------------------------------------------------------------------------------- /scaffolding/echotest/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | extern crate hocus_pocus; 4 | 5 | use hocus_pocus::LineBuffer; 6 | 7 | fn main() { 8 | let mut buffer = LineBuffer::new(String::from("Echo >>")).unwrap(); 9 | let mut string = String::new(); 10 | loop { 11 | match buffer.read_line(&mut string).unwrap() { 12 | 0 => break, 13 | _ => buffer.write_all(string.as_bytes()).unwrap(), 14 | } 15 | string.clear(); 16 | } 17 | buffer.write_all("\n".as_bytes()).unwrap(); 18 | } 19 | -------------------------------------------------------------------------------- /scaffolding/guernica.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withoutboats/notty/077b57357f3feff0a4b267d1faabaa0ddaf2b65e/scaffolding/guernica.jpg -------------------------------------------------------------------------------- /scaffolding/imagetest.rb: -------------------------------------------------------------------------------- 1 | require 'base64' 2 | 3 | img = Base64.strict_encode64(IO.binread('guernica.jpg')) 4 | mime = Base64.strict_encode64('image/jpeg') 5 | puts "\x1b_[14;80;16;4##{mime}##{img}\u{9c}" 6 | -------------------------------------------------------------------------------- /scaffolding/resources/update-config.toml: -------------------------------------------------------------------------------- 1 | [general] 2 | font = "Liberation Mono 8" 3 | -------------------------------------------------------------------------------- /scaffolding/src/cfg/mod.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::env; 3 | use std::path::PathBuf; 4 | 5 | use notty_cairo::Config as CairoConfig; 6 | 7 | mod toml; 8 | 9 | const CONFIG_FILE: &'static str = "scaffolding.toml"; 10 | 11 | pub struct Config { 12 | pub cairo: CairoConfig, 13 | pub shell: Cow<'static, str>, 14 | } 15 | 16 | impl Config { 17 | pub fn new() -> Config { 18 | let mut config = Config::default(); 19 | let user_config_path = match env::var("XDG_CONFIG_HOME") { 20 | Ok(dir) => PathBuf::from(dir).join(CONFIG_FILE), 21 | Err(_) => env::home_dir().unwrap().join(".config").join(CONFIG_FILE), 22 | }; 23 | let _ = toml::update_from_file(&mut config, user_config_path); 24 | config 25 | } 26 | } 27 | 28 | impl Default for Config { 29 | fn default() -> Config { 30 | Config { 31 | cairo: CairoConfig::default(), 32 | shell: Cow::Borrowed("sh"), 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /scaffolding/src/commands.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::io; 3 | use std::rc::Rc; 4 | use std::result; 5 | use std::sync::mpsc::Receiver; 6 | use std::sync::mpsc::TryRecvError::*; 7 | 8 | use gtk::{self, WidgetExt}; 9 | 10 | use notty::Command; 11 | use notty::terminal::Terminal; 12 | 13 | pub struct CommandApplicator { 14 | rx: Receiver, 15 | terminal: Rc>, 16 | canvas: Rc, 17 | } 18 | 19 | pub enum CommandError { 20 | Io(io::Error), 21 | Disconnected, 22 | } 23 | 24 | pub type Result = result::Result; 25 | 26 | impl CommandApplicator { 27 | 28 | pub fn new(rx: Receiver, 29 | terminal: Rc>, 30 | canvas: Rc) -> CommandApplicator { 31 | CommandApplicator { rx: rx, terminal: terminal, canvas: canvas } 32 | } 33 | 34 | pub fn apply(&self) -> Result<()> { 35 | let mut terminal = self.terminal.borrow_mut(); 36 | let mut redraw = false; 37 | loop { 38 | match self.rx.try_recv() { 39 | Ok(cmd) => { 40 | match terminal.apply(&cmd) { 41 | Err(e) => return Err(CommandError::Io(e)), 42 | _ => {}, 43 | } 44 | redraw = true; 45 | }, 46 | Err(Disconnected) => { 47 | return Err(CommandError::Disconnected); 48 | }, 49 | Err(Empty) => break, 50 | } 51 | } 52 | if redraw { self.canvas.queue_draw(); } 53 | Ok(()) 54 | } 55 | 56 | } 57 | 58 | unsafe impl Send for CommandApplicator { } 59 | -------------------------------------------------------------------------------- /scaffolding/src/key.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | use gdk::{EventKey, EventType, CONTROL_MASK}; 18 | use notty::datatypes::{Direction, Key}; 19 | use notty::Command; 20 | 21 | pub enum KeyEvent { 22 | Command(Command), 23 | Scroll(Direction), 24 | Copy, 25 | Paste, 26 | Ignore, 27 | } 28 | 29 | impl KeyEvent { 30 | pub fn new(event: &EventKey) -> KeyEvent { 31 | let ctor: fn(Key) -> Command = match event.get_event_type() { 32 | EventType::KeyPress => Command::key_press, 33 | EventType::KeyRelease => Command::key_release, 34 | _ => unreachable!() 35 | }; 36 | match (event.get_keyval(), shift_ctrl(event)) { 37 | // Shift+Ctrl+C 38 | (0x43, true) | (0x63, true) => KeyEvent::Copy, 39 | // Shift+Ctrl+V 40 | (0x56, true) | (0x76, true) => KeyEvent::Paste, 41 | // Shift+Ctrl+Up Arrow 42 | (0xff52, true) => KeyEvent::Scroll(Direction::Up), 43 | // Shift+Ctrl+Down Arrow 44 | (0xff54, true) => KeyEvent::Scroll(Direction::Down), 45 | // Shift+Ctrl+Left Arrow 46 | (0xff51, true) => KeyEvent::Scroll(Direction::Left), 47 | // Shift+Ctrl+Right Arrow 48 | (0xff53, true) => KeyEvent::Scroll(Direction::Right), 49 | (_, true) => KeyEvent::Ignore, 50 | (b @ 0x20...0x7e, _) => KeyEvent::Command(ctor(Key::Char(b as u8 as char))), 51 | (0xff08, _) => KeyEvent::Command(ctor(Key::Backspace)), 52 | (0xff09, _) => KeyEvent::Command(ctor(Key::Char('\x09'))), 53 | (0xff0a, _) | (0xff0d, _) => KeyEvent::Command(ctor(Key::Enter)), 54 | (0xff14, _) => KeyEvent::Command(ctor(Key::ScrollLock)), 55 | (0xff1b, _) => KeyEvent::Command(ctor(Key::Char('\x1b'))), 56 | (0xff50, _) => KeyEvent::Command(ctor(Key::Home)), 57 | (0xff51, _) => KeyEvent::Command(ctor(Key::LeftArrow)), 58 | (0xff52, _) => KeyEvent::Command(ctor(Key::UpArrow)), 59 | (0xff53, _) => KeyEvent::Command(ctor(Key::RightArrow)), 60 | (0xff54, _) => KeyEvent::Command(ctor(Key::DownArrow)), 61 | (0xff55, _) => KeyEvent::Command(ctor(Key::PageUp)), 62 | (0xff56, _) => KeyEvent::Command(ctor(Key::PageDown)), 63 | (0xff57, _) => KeyEvent::Command(ctor(Key::End)), 64 | (0xffe1, _) => KeyEvent::Command(ctor(Key::ShiftLeft)), 65 | (0xffe2, _) => KeyEvent::Command(ctor(Key::ShiftRight)), 66 | (0xffe3, _) => KeyEvent::Command(ctor(Key::CtrlLeft)), 67 | (0xffe4, _) => KeyEvent::Command(ctor(Key::CtrlRight)), 68 | (0xffe5, _) => KeyEvent::Command(ctor(Key::CapsLock)), 69 | (0xffe7, _) | (0xffeb, _) => KeyEvent::Command(ctor(Key::Meta)), 70 | (0xffe8, _) | (0xff67, _) => KeyEvent::Command(ctor(Key::Menu)), 71 | (0xffe9, _) => KeyEvent::Command(ctor(Key::AltLeft)), 72 | (0xffea, _) => KeyEvent::Command(ctor(Key::AltGr)), 73 | (0xffff, _) => KeyEvent::Command(ctor(Key::Delete)), 74 | (x, _) => { panic!("Key press: {:x}", x) } 75 | } 76 | } 77 | } 78 | 79 | // Returns true if this key event is while control and shift are held down 80 | fn shift_ctrl(event: &EventKey) -> bool { 81 | event.get_state().contains(CONTROL_MASK) && event.get_event_type() == EventType::KeyPress 82 | } 83 | -------------------------------------------------------------------------------- /scaffolding/src/main.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | extern crate cairo; 17 | extern crate gdk; 18 | extern crate glib; 19 | extern crate gtk; 20 | extern crate gtk_sys; 21 | extern crate toml; 22 | 23 | extern crate tty; 24 | extern crate notty; 25 | extern crate notty_cairo; 26 | 27 | use std::cell::RefCell; 28 | use std::env; 29 | use std::io::BufReader; 30 | use std::sync::Arc; 31 | use std::sync::atomic::{AtomicBool, Ordering}; 32 | use std::sync::mpsc; 33 | use std::rc::Rc; 34 | use std::thread; 35 | 36 | use gdk::Display; 37 | use gtk::{Clipboard, WindowExt, WidgetExt, ContainerExt}; 38 | 39 | use notty::{Command, Output}; 40 | use notty::terminal::Terminal; 41 | use notty_cairo::Renderer; 42 | 43 | mod cfg; 44 | mod commands; 45 | mod key; 46 | 47 | use commands::CommandApplicator; 48 | use key::KeyEvent; 49 | 50 | static mut X_PIXELS: Option = None; 51 | static mut Y_PIXELS: Option = None; 52 | 53 | static COLS: u32 = 80; 54 | static ROWS: u32 = 25; 55 | 56 | fn main() { 57 | 58 | // Read in configurations 59 | let config = cfg::Config::new(); 60 | 61 | // Set up window and drawing canvas. 62 | gtk::init().unwrap(); 63 | let window = gtk::Window::new(gtk::WindowType::Toplevel); 64 | let canvas = Rc::new(gtk::DrawingArea::new()); 65 | window.add(&*canvas); 66 | 67 | // Set the TERM variable and establish a TTY connection 68 | env::set_var("TERM", "notty"); 69 | 70 | let (tty_r, tty_w) = tty::pty(&config.shell, COLS as u16, ROWS as u16); 71 | 72 | // Handle program output (tty -> screen) on separate thread. 73 | let (tx_out, rx) = mpsc::channel(); 74 | let (tx_key_press, tx_key_release) = (tx_out.clone(), tx_out.clone()); 75 | 76 | let pty_open = Arc::new(AtomicBool::new(true)); 77 | let pty_open_checker = pty_open.clone(); 78 | thread::spawn(move || { 79 | let output = Output::new(BufReader::new(tty_r)); 80 | for result in output { 81 | match result { 82 | Ok(cmd) => { 83 | tx_out.send(cmd).unwrap(); 84 | }, 85 | Err(_) => { 86 | break; 87 | }, 88 | } 89 | } 90 | pty_open.store(false, Ordering::SeqCst); 91 | }); 92 | 93 | // Quit GTK main loop if the (tty -> screen) output handler thread indicates 94 | // pty is no longer open. 95 | glib::timeout_add(50, move || { 96 | match pty_open_checker.load(Ordering::SeqCst) { 97 | true => glib::Continue(true), 98 | false => { 99 | gtk::main_quit(); 100 | glib::Continue(false) 101 | } 102 | } 103 | }); 104 | 105 | // Set up logical terminal and renderer. 106 | let terminal = Rc::new(RefCell::new(Terminal::new(COLS, ROWS, tty_w))); 107 | let renderer = RefCell::new(Renderer::new(config.cairo)); 108 | 109 | // Process screen logic every 25 milliseconds. 110 | let cmd = CommandApplicator::new(rx, terminal.clone(), canvas.clone()); 111 | glib::timeout_add(25, move || { 112 | match cmd.apply() { 113 | Ok(_) => glib::Continue(true), 114 | Err(_) => { 115 | gtk::main_quit(); 116 | glib::Continue(false) 117 | } 118 | } 119 | }); 120 | 121 | // Connect signal to draw on canvas. 122 | canvas.connect_draw(move |_, canvas| { 123 | let mut terminal = terminal.borrow_mut(); 124 | if let (Some(x_pix), Some(y_pix)) = unsafe {(X_PIXELS.take(), Y_PIXELS.take())} { 125 | renderer.borrow_mut().reset_dimensions(&canvas, &mut terminal, x_pix, y_pix); 126 | } 127 | renderer.borrow_mut().draw(&terminal, &canvas); 128 | gtk::Inhibit(false) 129 | }); 130 | 131 | // Connect signal for changing window size. 132 | canvas.connect_configure_event(move |canvas, config| { 133 | unsafe { 134 | let (width, height) = config.get_size(); 135 | X_PIXELS = Some(width); 136 | Y_PIXELS = Some(height); 137 | } 138 | canvas.queue_draw(); 139 | false 140 | }); 141 | 142 | // Connect signal to receive key presses. 143 | let clipboard = Display::get_default().as_ref().and_then(Clipboard::get_default); 144 | window.connect_key_press_event(move |window, event| { 145 | match KeyEvent::new(event) { 146 | KeyEvent::Command(cmd) => tx_key_press.send(cmd).unwrap(), 147 | KeyEvent::Scroll(_) => println!("Scrolling is currently unimplemented"), 148 | KeyEvent::Copy => println!("Copying text is currently unimplemented"), 149 | KeyEvent::Paste => { 150 | if let Some(text) = clipboard.as_ref().and_then(Clipboard::wait_for_text) { 151 | tx_key_press.send(Command::paste(text)).unwrap(); 152 | } 153 | } 154 | KeyEvent::Ignore => window.queue_draw(), 155 | } 156 | gtk::Inhibit(false) 157 | }); 158 | 159 | // Connect signal to receive key releases. 160 | window.connect_key_release_event(move |window, event| { 161 | match KeyEvent::new(event) { 162 | KeyEvent::Command(cmd) => tx_key_release.send(cmd).unwrap(), 163 | _ => window.queue_draw(), 164 | } 165 | gtk::Inhibit(false) 166 | }); 167 | 168 | window.connect_delete_event(|_, _| { 169 | gtk::main_quit(); 170 | gtk::Inhibit(false) 171 | }); 172 | 173 | // Show the window and run the GTK event loop. 174 | window.set_default_size(800, 800); 175 | window.show_all(); 176 | gtk::main(); 177 | } 178 | -------------------------------------------------------------------------------- /scaffolding/tty/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tty" 3 | version = "0.1.0" 4 | authors = ["Without Boats "] 5 | license = "AGPL-3.0+" 6 | 7 | [dependencies] 8 | libc = "0.2.1" 9 | 10 | [dependencies.notty] 11 | path = "../.." 12 | -------------------------------------------------------------------------------- /scaffolding/tty/src/lib.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::io; 17 | use std::ffi::CString; 18 | use std::ops::Deref; 19 | use std::ptr; 20 | use std::sync::Arc; 21 | 22 | extern crate libc; 23 | extern crate notty; 24 | 25 | use notty::terminal::Tty; 26 | 27 | #[cfg(target_os="linux")] 28 | const TIOCSWINSZ: libc::c_ulong = 0x5414; 29 | #[cfg(target_os="macos")] 30 | const TIOCSWINSZ: libc::c_ulong = 2148037735; 31 | 32 | #[link(name="util")] 33 | extern { 34 | fn forkpty(amaster: *mut libc::c_int, 35 | name: *mut libc::c_char, 36 | termp: *const libc::c_void, 37 | winsize: *const Winsize) -> libc::pid_t; 38 | } 39 | 40 | #[repr(C)] 41 | struct Winsize { 42 | ws_row: libc::c_ushort, 43 | ws_col: libc::c_ushort, 44 | ws_xpixel: libc::c_ushort, 45 | ws_ypixel: libc::c_ushort, 46 | } 47 | 48 | pub fn pty(name: &str, width: u16, height: u16) -> (Reader, Writer) { 49 | let mut amaster = 0; 50 | let winsize = Winsize { 51 | ws_row: height as libc::c_ushort, 52 | ws_col: width as libc::c_ushort, 53 | ws_xpixel: 0, 54 | ws_ypixel: 0 55 | }; 56 | match unsafe { 57 | forkpty(&mut amaster as *mut _, 58 | ptr::null_mut(), 59 | ptr::null(), 60 | &winsize as *const _) 61 | } { 62 | 0 => { 63 | let name = CString::new(name).unwrap(); 64 | unsafe { 65 | libc::execvp(name.as_ptr(), ptr::null()); 66 | } 67 | unreachable!(); 68 | } 69 | n if n > 0 => { 70 | let handle = Arc::new(Handle(amaster)); 71 | (Reader(handle.clone()), Writer(handle.clone())) 72 | } 73 | _ => panic!("Fork failed.") 74 | } 75 | } 76 | 77 | pub struct Reader(Arc); 78 | 79 | impl Deref for Reader { 80 | type Target = Handle; 81 | fn deref(&self) -> &Handle { 82 | &*self.0 83 | } 84 | } 85 | 86 | impl io::Read for Reader { 87 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 88 | match unsafe { 89 | libc::read(**self.0, 90 | buf.as_mut_ptr() as *mut _, 91 | buf.len() as libc::size_t) 92 | } { 93 | n if n >= 0 => Ok(n as usize), 94 | _ => Err(io::Error::last_os_error()), 95 | } 96 | } 97 | } 98 | 99 | pub struct Writer(Arc); 100 | 101 | impl Deref for Writer { 102 | type Target = Handle; 103 | fn deref(&self) -> &Handle { 104 | &*self.0 105 | } 106 | } 107 | 108 | impl io::Write for Writer { 109 | fn write(&mut self, buf: &[u8]) -> io::Result { 110 | match unsafe { 111 | libc::write(**self.0, 112 | buf.as_ptr() as *const _, 113 | buf.len() as libc::size_t) 114 | } { 115 | n if n >= 0 => Ok(n as usize), 116 | _ => Err(io::Error::last_os_error()), 117 | } 118 | } 119 | fn flush(&mut self) -> io::Result<()> { Ok(()) } 120 | } 121 | 122 | impl Tty for Writer { 123 | fn set_winsize(&mut self, width: u16, height: u16) -> io::Result<()> { 124 | (**self).set_winsize(width, height) 125 | } 126 | } 127 | 128 | pub struct Handle(libc::c_int); 129 | 130 | impl Handle { 131 | pub fn set_winsize(&self, width: u16, height: u16) -> io::Result<()> { 132 | let winsize = Winsize { 133 | ws_row: height as libc::c_ushort, 134 | ws_col: width as libc::c_ushort, 135 | ws_xpixel: 0, 136 | ws_ypixel: 0 137 | }; 138 | match unsafe { 139 | libc::ioctl(**self, 140 | TIOCSWINSZ, 141 | &winsize as *const _) 142 | } { 143 | -1 => Err(io::Error::last_os_error()), 144 | _ => Ok(()), 145 | } 146 | } 147 | } 148 | 149 | impl Deref for Handle { 150 | type Target = libc::c_int; 151 | fn deref(&self) -> &libc::c_int { &self.0 } 152 | } 153 | 154 | impl Drop for Handle { 155 | fn drop(&mut self) { 156 | unsafe { 157 | libc::close(self.0); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withoutboats/notty/077b57357f3feff0a4b267d1faabaa0ddaf2b65e/screenshot.png -------------------------------------------------------------------------------- /src/command/erase.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use notty_encoding::cmds::{Erase, RemoveChars, RemoveRows, InsertBlank, InsertRows}; 17 | 18 | use command::prelude::*; 19 | 20 | impl Command for Erase { 21 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 22 | terminal.erase(self.area); 23 | Ok(()) 24 | } 25 | 26 | #[cfg(any(test, debug_assertions))] 27 | fn repr(&self) -> String { 28 | String::from("ERASE") 29 | } 30 | } 31 | 32 | impl Command for RemoveChars { 33 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 34 | terminal.remove_at(self.count); 35 | Ok(()) 36 | } 37 | 38 | #[cfg(any(test, debug_assertions))] 39 | fn repr(&self) -> String { 40 | format!("REMOVE {} CHARS", self.count) 41 | } 42 | } 43 | 44 | impl Command for RemoveRows { 45 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 46 | terminal.remove_rows_at(self.count, self.include); 47 | Ok(()) 48 | } 49 | 50 | #[cfg(any(test, debug_assertions))] 51 | fn repr(&self) -> String { 52 | match self.include { 53 | true => format!("REMOVE {} ROWS INCL CURSOR", self.count), 54 | false => format!("REMOVE {} ROWS BELOW CURSOR", self.count), 55 | } 56 | } 57 | } 58 | 59 | impl Command for InsertBlank { 60 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 61 | terminal.insert_blank_at(self.count); 62 | Ok(()) 63 | } 64 | 65 | #[cfg(any(test, debug_assertions))] 66 | fn repr(&self) -> String { 67 | format!("INSERT {} BLANK SPACES", self.count) 68 | } 69 | } 70 | 71 | impl Command for InsertRows { 72 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 73 | terminal.insert_rows_at(self.count, self.include); 74 | Ok(()) 75 | } 76 | 77 | #[cfg(any(test, debug_assertions))] 78 | fn repr(&self) -> String { 79 | match self.include { 80 | true => format!("INSERT {} ROWS ABOVE CURSOR", self.count), 81 | false => format!("INSERT {} ROWS BELOW CURSOR", self.count), 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/command/input.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use command::prelude::*; 17 | use datatypes::Key; 18 | 19 | pub struct KeyPress(pub Key); 20 | 21 | impl Command for KeyPress { 22 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 23 | terminal.send_input(self.0.clone(), true) 24 | } 25 | 26 | #[cfg(any(test, debug_assertions))] 27 | fn repr(&self) -> String { 28 | String::from("KEY PRESS") 29 | } 30 | } 31 | 32 | pub struct KeyRelease(pub Key); 33 | 34 | impl Command for KeyRelease { 35 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 36 | terminal.send_input(self.0.clone(), false) 37 | } 38 | 39 | #[cfg(any(test, debug_assertions))] 40 | fn repr(&self) -> String { 41 | String::from("KEY RELEASE") 42 | } 43 | } 44 | 45 | pub struct Paste(pub String); 46 | 47 | impl Command for Paste { 48 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 49 | terminal.paste(&self.0) 50 | } 51 | 52 | #[cfg(any(test, debug_assertions))] 53 | fn repr(&self) -> String { 54 | String::from("PASTE") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/command/meta.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::cell::RefCell; 17 | 18 | use notty_encoding::cmds::SetInputMode; 19 | 20 | use command::prelude::*; 21 | #[cfg(any(test, debug_assertions))] 22 | use datatypes::InputSettings; 23 | 24 | pub struct SetTitle(pub RefCell>); 25 | 26 | impl Command for SetTitle { 27 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 28 | if let Some(title) = self.0.borrow_mut().take() { 29 | terminal.set_title(title); 30 | } 31 | Ok(()) 32 | } 33 | 34 | #[cfg(any(test, debug_assertions))] 35 | fn repr(&self) -> String { 36 | String::from("SET TITLE") 37 | } 38 | } 39 | 40 | impl Command for SetInputMode { 41 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 42 | terminal.set_input_mode(self.0); 43 | Ok(()) 44 | } 45 | 46 | #[cfg(any(test, debug_assertions))] 47 | fn repr(&self) -> String { 48 | match self.0 { 49 | InputSettings::Ansi(false) => String::from("SET MODE ANSI"), 50 | InputSettings::Ansi(true) => String::from("SET MODE APPLICATION"), 51 | InputSettings::Notty(_) => String::from("SET MODE EXTENDED"), 52 | InputSettings::LineBufferEcho(_, _) => String::from("SET MODE LINEBUFFER ECHO"), 53 | InputSettings::ScreenEcho(_) => String::from("SET MODE SCREEN ECHO"), 54 | InputSettings::BracketedPasteMode(_) => String::from("SET BRACKETED PASTE MODE"), 55 | } 56 | } 57 | } 58 | 59 | #[derive(Default, Copy, Clone)] 60 | pub struct Bell; 61 | 62 | impl Command for Bell { 63 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 64 | terminal.bell(); 65 | Ok(()) 66 | } 67 | 68 | #[cfg(any(test, debug_assertions))] 69 | fn repr(&self) -> String { 70 | String::from("BELL") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/command/mod.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::io; 17 | 18 | use terminal::Terminal; 19 | use Command; 20 | 21 | mod erase; 22 | mod input; 23 | mod meta; 24 | mod movement; 25 | mod panel; 26 | mod put; 27 | mod respond; 28 | mod style; 29 | mod tooltip; 30 | 31 | pub use notty_encoding::cmds::{ 32 | Erase, RemoveChars, RemoveRows, InsertBlank, InsertRows, 33 | PushPanel, PopPanel, SplitPanel, UnsplitPanel, AdjustPanelSplit, 34 | RotateSectionDown, RotateSectionUp, SwitchActiveSection, 35 | SetInputMode, 36 | Move, ScrollScreen, 37 | SetCursorStyle, DefaultCursorStyle, 38 | SetTextStyle, DefaultTextStyle, 39 | SetStyleInArea, DefaultStyleInArea, 40 | }; 41 | 42 | pub use self::input::{KeyPress, KeyRelease, Paste}; 43 | pub use self::meta::{SetTitle, Bell}; 44 | pub use self::put::{Put, PutAt}; 45 | pub use self::respond::{StaticResponse, ReportPosition}; 46 | pub use self::tooltip::{AddToolTip, RemoveToolTip, AddDropDown}; 47 | 48 | mod prelude { 49 | pub use std::io; 50 | pub use terminal::Terminal; 51 | pub use super::CommandTrait as Command; 52 | } 53 | 54 | pub trait CommandTrait: Send + 'static { 55 | fn apply(&self, &mut Terminal) -> io::Result<()>; 56 | #[cfg(any(test, debug_assertions))] 57 | fn repr(&self) -> String; 58 | } 59 | 60 | pub struct CommandSeries(pub Vec); 61 | 62 | impl CommandTrait for CommandSeries { 63 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 64 | for cmd in &self.0 { 65 | try!(terminal.apply(cmd)); 66 | } 67 | Ok(()) 68 | } 69 | 70 | #[cfg(any(test, debug_assertions))] 71 | fn repr(&self) -> String { 72 | String::from("SERIES: ") + &self.0.iter().map(|c| c.inner.repr()) 73 | .collect::>().join("; ") 74 | } 75 | } 76 | 77 | pub struct NoFeature(pub String); 78 | 79 | impl CommandTrait for NoFeature { 80 | fn apply(&self, _: &mut Terminal) -> io::Result<()> { 81 | Ok(()) 82 | } 83 | 84 | #[cfg(any(test, debug_assertions))] 85 | fn repr(&self) -> String { 86 | format!("NO FEATURE: {}", self.0) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/command/movement.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use notty_encoding::cmds::{Move, ScrollScreen}; 17 | 18 | use command::prelude::*; 19 | #[cfg(any(test, debug_assertions))] 20 | use datatypes::Direction::*; 21 | #[cfg(any(test, debug_assertions))] 22 | use datatypes::Movement::*; 23 | 24 | impl Command for Move { 25 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 26 | terminal.move_cursor(self.movement); 27 | Ok(()) 28 | } 29 | 30 | #[cfg(any(test, debug_assertions))] 31 | fn repr(&self) -> String { 32 | match self.movement { 33 | To(Up, n, _) => format!("MOVE UP {}", n), 34 | To(Down, n, _) => format!("MOVE DOWN {}", n), 35 | To(Left, n, _) => format!("MOVE LEFT {}", n), 36 | To(Right, n, _) => format!("MOVE RIGHT {}", n), 37 | PreviousLine(n) => format!("MOVE PREV LINE {}", n), 38 | NextLine(n) => format!("MOVE NEXT LINE {}", n), 39 | Tab(Up, n, _) => format!("MOVE UP TAB {}", n), 40 | Tab(Down, n, _) => format!("MOVE DOWN TAB {}", n), 41 | Tab(Left, n, _) => format!("MOVE LEFT TAB {}", n), 42 | Tab(Right, n, _) => format!("MOVE RIGHT TAB {}", n), 43 | IndexTo(Up, n) => format!("MOVE UP INDEX {}", n), 44 | IndexTo(Down, n) => format!("MOVE DOWN INDEX {}", n), 45 | IndexTo(Left, n) => format!("MOVE LEFT INDEX {}", n), 46 | IndexTo(Right, n) => format!("MOVE RIGHT INDEX {}", n), 47 | Column(n) => format!("MOVE TO COL {}", n), 48 | Row(n) => format!("MOVE TO ROW {}", n), 49 | Position(coords) => format!("MOVE TO {},{}", coords.x, coords.y), 50 | ToEdge(Up) => String::from("MOVE UP TO EDGE"), 51 | ToEdge(Down) => String::from("MOVE DOWN TO EDGE"), 52 | ToEdge(Left) => String::from("MOVE LEFT TO EDGE"), 53 | ToEdge(Right) => String::from("MOVE RIGHT TO EDGE"), 54 | ToBeginning => String::from("MOVE TO BEGINNING"), 55 | ToEnd => String::from("MOVE TO END"), 56 | } 57 | } 58 | } 59 | 60 | impl Command for ScrollScreen { 61 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 62 | terminal.scroll(self.dir, self.n); 63 | Ok(()) 64 | } 65 | 66 | #[cfg(any(test, debug_assertions))] 67 | fn repr(&self) -> String { 68 | String::from("SCROLL SCREEN") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/command/panel.rs: -------------------------------------------------------------------------------- 1 | use command::prelude::*; 2 | use datatypes::{SaveGrid, ResizeRule}; 3 | 4 | use notty_encoding::cmds::{ 5 | PushPanel, PopPanel, 6 | SplitPanel, UnsplitPanel, AdjustPanelSplit, 7 | RotateSectionDown, RotateSectionUp, 8 | SwitchActiveSection, 9 | }; 10 | 11 | impl Command for PushPanel { 12 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 13 | terminal.push(self.0, self.1.unwrap_or(true)); 14 | Ok(()) 15 | } 16 | 17 | #[cfg(any(test, debug_assertions))] 18 | fn repr(&self) -> String { 19 | String::from("PUSH BUFFER") 20 | } 21 | } 22 | 23 | impl Command for PopPanel { 24 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 25 | terminal.pop(self.0); 26 | Ok(()) 27 | } 28 | 29 | #[cfg(any(test, debug_assertions))] 30 | fn repr(&self) -> String { 31 | String::from("POP BUFFER") 32 | } 33 | } 34 | 35 | impl Command for SplitPanel { 36 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 37 | let save = self.save.unwrap_or(SaveGrid::Left); 38 | let rule = self.rule.unwrap_or(ResizeRule::Percentage); 39 | terminal.split(save, self.kind, rule, self.split_tag, self.l_tag, self.r_tag, 40 | self.retain_offscreen_state.unwrap_or(true)); 41 | Ok(()) 42 | } 43 | 44 | #[cfg(any(test, debug_assertions))] 45 | fn repr(&self) -> String { 46 | String::from("SPLIT BUFFER") 47 | } 48 | } 49 | 50 | impl Command for UnsplitPanel { 51 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 52 | terminal.unsplit(self.save, self.unsplit_tag); 53 | Ok(()) 54 | } 55 | 56 | #[cfg(any(test, debug_assertions))] 57 | fn repr(&self) -> String { 58 | String::from("UNSPLIT BUFFER") 59 | } 60 | } 61 | 62 | impl Command for AdjustPanelSplit { 63 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 64 | terminal.adjust_split(self.adjust_tag, self.kind); 65 | Ok(()) 66 | } 67 | 68 | #[cfg(any(test, debug_assertions))] 69 | fn repr(&self) -> String { 70 | String::from("ADJUST PANEL SPLIT") 71 | } 72 | } 73 | 74 | impl Command for RotateSectionDown { 75 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 76 | terminal.rotate_down(self.0); 77 | Ok(()) 78 | } 79 | 80 | #[cfg(any(test, debug_assertions))] 81 | fn repr(&self) -> String { 82 | String::from("ROTATE DOWN") 83 | } 84 | } 85 | 86 | impl Command for RotateSectionUp { 87 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 88 | terminal.rotate_up(self.0); 89 | Ok(()) 90 | } 91 | 92 | #[cfg(any(test, debug_assertions))] 93 | fn repr(&self) -> String { 94 | String::from("ROTATE UP") 95 | } 96 | } 97 | 98 | impl Command for SwitchActiveSection { 99 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 100 | terminal.switch(self.0); 101 | Ok(()) 102 | } 103 | 104 | #[cfg(any(test, debug_assertions))] 105 | fn repr(&self) -> String { 106 | format!("SWITCH TO PANEL {}", self.0) 107 | } 108 | } 109 | 110 | -------------------------------------------------------------------------------- /src/command/put.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use mime::Mime; 17 | 18 | use command::prelude::*; 19 | use datatypes::{Coords, MediaPosition}; 20 | use datatypes::Movement::Position; 21 | use terminal::{CharData, WideChar, CharExtender, Image}; 22 | 23 | pub struct Put(T); 24 | 25 | impl Put { 26 | pub fn new_char(ch: char) -> Put { 27 | Put(ch) 28 | } 29 | } 30 | 31 | impl Put { 32 | pub fn new_wide_char(ch: char, width: u32) -> Put { 33 | Put(WideChar::new(ch, width)) 34 | } 35 | } 36 | 37 | impl Put { 38 | pub fn new_extender(ch: char) -> Put { 39 | Put(CharExtender::new(ch)) 40 | } 41 | } 42 | 43 | impl Put { 44 | pub fn new_image(data: Vec, mime: Mime, pos: MediaPosition, w: u32, h: u32) -> Put { 45 | Put(Image::new(data, mime, pos, w, h)) 46 | } 47 | } 48 | 49 | impl Command for Put { 50 | 51 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 52 | terminal.write(&self.0); 53 | Ok(()) 54 | } 55 | 56 | #[cfg(any(test, debug_assertions))] 57 | fn repr(&self) -> String { 58 | self.0.repr() 59 | } 60 | 61 | } 62 | 63 | pub struct PutAt(T, Coords); 64 | 65 | impl PutAt { 66 | 67 | pub fn new_image(data: Vec, mime: Mime, pos: MediaPosition, w: u32, h: u32, at: Coords) 68 | -> PutAt { 69 | PutAt(Image::new(data, mime, pos, w, h), at) 70 | } 71 | } 72 | 73 | impl Command for PutAt { 74 | 75 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 76 | let coords = terminal.cursor().position(); 77 | terminal.move_cursor(Position(self.1)); 78 | terminal.write(&self.0); 79 | terminal.move_cursor(Position(coords)); 80 | Ok(()) 81 | } 82 | 83 | #[cfg(any(test, debug_assertions))] 84 | fn repr(&self) -> String { 85 | String::from("PUT AT") 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/command/respond.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::borrow::Cow; 17 | 18 | use command::prelude::*; 19 | use datatypes::{Key, Coords, Code}; 20 | 21 | pub struct StaticResponse(pub &'static str); 22 | 23 | impl Command for StaticResponse { 24 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 25 | terminal.send_input(Key::Cmd(Cow::Borrowed(self.0)), true) 26 | } 27 | 28 | #[cfg(any(test, debug_assertions))] 29 | fn repr(&self) -> String { 30 | String::from("RESPOND ") + self.0 31 | } 32 | } 33 | 34 | pub struct ReportPosition(pub Code); 35 | 36 | impl Command for ReportPosition { 37 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 38 | let Coords { x, y } = terminal.cursor().position(); 39 | let cmd = match self.0 { 40 | Code::ANSI => Cow::Owned(format!("\x1b[{};{}R", y, x)), 41 | _ => unimplemented!(), 42 | }; 43 | terminal.send_input(Key::Cmd(cmd), true) 44 | } 45 | 46 | #[cfg(any(test, debug_assertions))] 47 | fn repr(&self) -> String { 48 | String::from("REPORT POSITION") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/command/style.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use terminal::interfaces::Styleable; 17 | 18 | use notty_encoding::cmds::{ 19 | SetCursorStyle, DefaultCursorStyle, 20 | SetTextStyle, DefaultTextStyle, 21 | SetStyleInArea, DefaultStyleInArea, 22 | }; 23 | 24 | use command::prelude::*; 25 | 26 | impl Command for SetCursorStyle { 27 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 28 | terminal.cursor_mut().set_style(self.0); 29 | Ok(()) 30 | } 31 | 32 | #[cfg(any(test, debug_assertions))] 33 | fn repr(&self) -> String { 34 | String::from("SET CURSOR STYLE") 35 | } 36 | } 37 | 38 | impl Command for DefaultCursorStyle { 39 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 40 | terminal.cursor_mut().reset_style(); 41 | Ok(()) 42 | } 43 | 44 | #[cfg(any(test, debug_assertions))] 45 | fn repr(&self) -> String { 46 | String::from("DEFAULT CURSOR STYLE") 47 | } 48 | } 49 | 50 | impl Command for SetTextStyle { 51 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 52 | terminal.set_style(self.0); 53 | Ok(()) 54 | } 55 | 56 | #[cfg(any(test, debug_assertions))] 57 | fn repr(&self) -> String { 58 | String::from("SET TEXT STYLE") 59 | } 60 | } 61 | 62 | impl Command for DefaultTextStyle { 63 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 64 | terminal.reset_style(); 65 | Ok(()) 66 | } 67 | 68 | #[cfg(any(test, debug_assertions))] 69 | fn repr(&self) -> String { 70 | String::from("DEFAULT TEXT STYLE") 71 | } 72 | } 73 | 74 | impl Command for SetStyleInArea { 75 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 76 | terminal.set_style_in_area(self.0, self.1); 77 | Ok(()) 78 | } 79 | 80 | #[cfg(any(test, debug_assertions))] 81 | fn repr(&self) -> String { 82 | String::from("SET STYLE IN AREA") 83 | } 84 | } 85 | 86 | impl Command for DefaultStyleInArea { 87 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 88 | terminal.reset_styles_in_area(self.0); 89 | Ok(()) 90 | } 91 | 92 | #[cfg(any(test, debug_assertions))] 93 | fn repr(&self) -> String { 94 | String::from("DEFAULT STYLE IN AREA") 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/command/tooltip.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::cell::RefCell; 17 | 18 | use command::prelude::*; 19 | use datatypes::Coords; 20 | 21 | pub struct AddToolTip(pub Coords, pub RefCell>); 22 | 23 | impl Command for AddToolTip { 24 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 25 | if let Some(string) = self.1.borrow_mut().take() { 26 | terminal.add_tooltip(self.0, string); 27 | } 28 | Ok(()) 29 | } 30 | 31 | #[cfg(any(test, debug_assertions))] 32 | fn repr(&self) -> String { 33 | String::from("ADD TOOL TIP") 34 | } 35 | } 36 | 37 | pub struct RemoveToolTip(pub Coords); 38 | 39 | impl Command for RemoveToolTip { 40 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 41 | terminal.remove_tooltip(self.0); 42 | Ok(()) 43 | } 44 | 45 | #[cfg(any(test, debug_assertions))] 46 | fn repr(&self) -> String { 47 | String::from("REMOVE TOOL TIP") 48 | } 49 | } 50 | 51 | pub struct AddDropDown { 52 | pub coords: Coords, 53 | pub options: RefCell>>, 54 | } 55 | 56 | impl Command for AddDropDown { 57 | fn apply(&self, terminal: &mut Terminal) -> io::Result<()> { 58 | if let Some(options) = self.options.borrow_mut().take() { 59 | terminal.add_drop_down(self.coords, options); 60 | } 61 | Ok(()) 62 | } 63 | 64 | #[cfg(any(test, debug_assertions))] 65 | fn repr(&self) -> String { 66 | String::from("ADD TOOL TIP - DROP DOWN MENU") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/datatypes/key.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::borrow::Cow; 17 | 18 | use self::Key::*; 19 | 20 | /// Mostly, these represent keys on the keyboard. Boolean fields are true for key presses and 21 | /// false for key releases. 22 | #[derive(Clone, Debug, Eq, PartialEq)] 23 | pub enum Key { 24 | /// Any key which generates a unicode code point when pressed. 25 | Char(char), 26 | UpArrow, 27 | DownArrow, 28 | LeftArrow, 29 | RightArrow, 30 | Enter, 31 | Backspace, 32 | ShiftLeft, 33 | ShiftRight, 34 | CtrlLeft, 35 | CtrlRight, 36 | AltLeft, 37 | AltGr, 38 | Meta, 39 | Menu, 40 | PageUp, 41 | PageDown, 42 | Home, 43 | End, 44 | Insert, 45 | Delete, 46 | CapsLock, 47 | NumLock, 48 | ScrollLock, 49 | /// A function; the byte value indicates the key number. 50 | Function(u8), 51 | /// This key is not generated by the keyboard but as a response to some escape code sent 52 | /// to the output. 53 | Cmd(Cow<'static, str>), 54 | /// A selection from an in-terminal drop down menu. 55 | MenuSelection(usize), 56 | } 57 | 58 | 59 | impl Key { 60 | pub fn is_modifier(&self) -> bool { 61 | match *self { 62 | ShiftLeft | ShiftRight | CtrlLeft | CtrlRight | AltLeft | AltGr | CapsLock => true, 63 | _ => false 64 | } 65 | } 66 | 67 | pub fn ctrl_modify(self) -> Key { 68 | match self { 69 | Key::Char(c @ '\x40'...'\x7f') 70 | => Key::Char((c as u8 & 0x1f) as char), 71 | _ => self 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/datatypes/mod.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | //! The datatypes module defines the abstract datatypes used by other components of notty. 17 | //! 18 | //! The types in this module are intended to be passed between modules. As a design restriction, 19 | //! any methods on any type in this submodule are required to take the receiver immutably. 20 | use std::cmp; 21 | use std::sync::atomic::Ordering::Relaxed; 22 | 23 | mod iter; 24 | mod key; 25 | 26 | use cfg::TAB_STOP; 27 | 28 | pub use self::iter::CoordsIter; 29 | pub use self::key::Key; 30 | 31 | pub use notty_encoding::args::*; 32 | 33 | pub mod args { 34 | pub use super::{ 35 | Area, 36 | BufferSettings, 37 | CodeGroup, 38 | Color, 39 | ConfigStyle, 40 | Coords, 41 | Direction, 42 | EchoSettings, 43 | InputSettings, 44 | MediaAlignment, 45 | MediaPosition, 46 | Movement, 47 | Region, 48 | ResizeRule, 49 | SaveGrid, 50 | SplitKind, 51 | Style, 52 | }; 53 | pub use super::Area::*; 54 | pub use super::Direction::*; 55 | pub use super::InputSettings::*; 56 | pub use super::MediaAlignment::*; 57 | pub use super::MediaPosition::*; 58 | pub use super::Movement::*; 59 | pub use super::Style::*; 60 | pub use notty_encoding::args::Argument; 61 | } 62 | 63 | /// A kind of escape code format (used for structuring response strings). 64 | #[derive(Copy, Clone, Eq, PartialEq)] 65 | pub enum Code { 66 | ANSI, 67 | Notty, 68 | } 69 | 70 | /// Calculate the movement from one coordinate to another within a region. 71 | pub fn move_within(Coords {x, y}: Coords, movement: Movement, region: Region) -> Coords { 72 | use self::Movement::*; 73 | use self::Direction::*; 74 | let tab_stop = TAB_STOP.load(Relaxed) as u32; 75 | match movement { 76 | Position(coords) => region.xy_within(coords), 77 | Column(n) => Coords {x: region.x_within(n), y: y}, 78 | Row(n) => Coords {x: x, y: region.y_within(n)}, 79 | ToEdge(Up) => Coords {x: x, y: region.top}, 80 | ToEdge(Down) => Coords {x: x, y: region.bottom - 1}, 81 | ToEdge(Left) => Coords {x: region.left, y: y}, 82 | ToEdge(Right) => Coords {x: region.right - 1, y: y}, 83 | ToBeginning => Coords {x: region.left, y: region.top}, 84 | ToEnd => Coords {x: region.right - 1, y: region.bottom - 1}, 85 | To(Up, n, true) if region.top + n > y => { 86 | let x = x.saturating_sub((region.top + n - y) / (region.bottom - region.top) + 1); 87 | let y = region.bottom - (region.top + n - y) % (region.bottom - region.top); 88 | if x < region.left { 89 | Coords { x: region.left, y: region.top } 90 | } else { 91 | Coords { x: x, y: y } 92 | } 93 | } 94 | To(Down, n, true) if y + n >= region.bottom => { 95 | let x = x + (y + n - region.bottom) / (region.bottom - region.top) + 1; 96 | let y = region.top + (y + n - region.bottom) % (region.bottom - region.top); 97 | if x >= region.right { 98 | Coords { x: region.right - 1, y: region.bottom - 1 } 99 | } else { 100 | Coords { x: x, y: y } 101 | } 102 | } 103 | To(Left, n, true) if region.left + n > x => { 104 | let y = y.saturating_sub((region.left + n - x) / (region.right - region.left) + 1); 105 | let x = region.right - (region.left + n - x) % (region.right - region.left); 106 | if y < region.top { 107 | Coords { x: region.left, y: region.top } 108 | } else { 109 | Coords { x: x, y: y } 110 | } 111 | } 112 | To(Right, n, true) if x + n >= region.right => { 113 | let y = y + (x + n - region.right) / (region.right - region.left) + 1; 114 | let x = region.left + (x + n - region.right) % (region.right - region.left); 115 | if y >= region.bottom { 116 | Coords { x: region.right - 1, y: region.bottom - 1 } 117 | } else { 118 | Coords { x: x, y: y } 119 | } 120 | } 121 | To(Up, n, _) | IndexTo(Up, n) => { 122 | Coords {x: x, y: cmp::max(region.top, y.saturating_sub(n))} 123 | } 124 | To(Down, n, _) | IndexTo(Down, n) => { 125 | Coords {x: x, y: cmp::min(y.saturating_add(n), region.bottom - 1)} 126 | } 127 | To(Left, n, _) | IndexTo(Left, n) => { 128 | Coords {x: cmp::max(region.left, x.saturating_sub(n)), y: y} 129 | } 130 | To(Right, n, _) | IndexTo(Right, n) => { 131 | Coords {x: cmp::min(x.saturating_add(n), region.right - 1), y: y} 132 | } 133 | Tab(Left, n, true) if region.left + n > x => { 134 | unimplemented!() 135 | } 136 | Tab(Right, n, true) if x + n >= region.right => { 137 | unimplemented!() 138 | } 139 | Tab(Left, n, _) => { 140 | let tab = ((x / tab_stop).saturating_sub(n)) * tab_stop; 141 | Coords {x: cmp::max(tab, region.left), y: y} 142 | } 143 | Tab(Right, n, _) => { 144 | let tab = ((x / tab_stop) + n) * tab_stop; 145 | Coords {x: cmp::min(tab, region.right - 1), y: y} 146 | } 147 | Tab(..) => unimplemented!(), 148 | PreviousLine(n) => { 149 | Coords {x: 0, y: cmp::max(y.saturating_sub(n), region.top)} 150 | } 151 | NextLine(n) => { 152 | Coords {x: 0, y: cmp::min(y.saturating_add(n), region.bottom - 1)} 153 | } 154 | } 155 | } 156 | 157 | #[derive(Copy, Clone)] 158 | pub struct GridSettings { 159 | pub width: u32, 160 | pub height: u32, 161 | pub retain_offscreen_state: bool, 162 | pub flow: Flow, 163 | } 164 | 165 | #[derive(Eq, PartialEq, Debug, Copy, Clone)] 166 | pub enum Flow { 167 | Moveable, 168 | Reflowable, 169 | } 170 | 171 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | #![feature(io, pub_restricted, associated_consts)] 17 | 18 | extern crate base64; 19 | extern crate mime; 20 | extern crate notty_encoding; 21 | extern crate unicode_width; 22 | extern crate uuid; 23 | 24 | mod command; 25 | pub mod datatypes; 26 | mod grapheme_tables; 27 | mod output; 28 | pub mod terminal; 29 | 30 | pub use output::Output; 31 | 32 | use command::{KeyPress, KeyRelease, Paste, CommandTrait}; 33 | use datatypes::Key; 34 | 35 | /// A command to be applied to the terminal. 36 | /// 37 | /// `Command` is an opaque wrapper type around the various commands, which does not allow for 38 | /// introspection or reflection. The `Output` iterator generates `Command` objects transmitted from 39 | /// the output of the controlling process. User input is also represented as a command, and 40 | /// constructors exist for creating `Command` objects with the correct internal representation for 41 | /// different kinds of user input. 42 | /// 43 | /// Commands are passed to the `Terminal` with the `apply` method. 44 | pub struct Command { 45 | inner: Box, 46 | } 47 | 48 | impl Command { 49 | /// Create a command representing a key press event. 50 | pub fn key_press(key: Key) -> Command { 51 | Command { 52 | inner: Box::new(KeyPress(key)) as Box, 53 | } 54 | } 55 | /// Create a command representing a key release event. 56 | pub fn key_release(key: Key) -> Command { 57 | Command { 58 | inner: Box::new(KeyRelease(key)) as Box, 59 | } 60 | } 61 | 62 | /// Create a command representing a paste from a clipboard. 63 | pub fn paste(data: String) -> Command { 64 | Command { 65 | inner: Box::new(Paste(data)) as Box, 66 | } 67 | } 68 | } 69 | 70 | pub mod cfg { 71 | use std::sync::atomic::{AtomicIsize, AtomicUsize, ATOMIC_ISIZE_INIT, ATOMIC_USIZE_INIT}; 72 | 73 | /// The amount of scrollback to save in terminal grids which save their scrollback. None and 74 | /// 0 mean that an infinite scrollback will be saved. If this is not set, it will be treated as 75 | /// infinite scrollback. 76 | pub static SCROLLBACK: AtomicIsize = ATOMIC_ISIZE_INIT; 77 | 78 | /// The distance between each tab stop. If you do not set this to a non-zero value, it will be 79 | /// set to 4 when the terminal is initialized. 80 | pub static TAB_STOP: AtomicUsize = ATOMIC_USIZE_INIT; 81 | } 82 | -------------------------------------------------------------------------------- /src/output/notty/attachment.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use base64; 17 | 18 | pub struct Attachments { 19 | data: Vec, 20 | } 21 | 22 | impl Default for Attachments { 23 | fn default() -> Attachments { 24 | Attachments { data: vec![String::new()] } 25 | } 26 | } 27 | 28 | impl Attachments { 29 | 30 | pub fn clear(&mut self) { 31 | self.data.truncate(1); 32 | self.data.last_mut().unwrap().clear(); 33 | } 34 | 35 | pub fn iter(&self) -> AttachmentIter { 36 | self.into_iter() 37 | } 38 | 39 | pub fn append(&mut self, ch: char) -> Option { 40 | match ch { 41 | '0'...'9' | 'A'...'Z' | 'a'...'z' | '+' | '/' | '=' => { 42 | self.data.last_mut().unwrap().push(ch); 43 | None 44 | } 45 | '#' => { 46 | self.data.push(String::new()); 47 | None 48 | } 49 | '\u{9c}' => Some(true), 50 | _ => Some(false), 51 | } 52 | } 53 | 54 | } 55 | 56 | impl<'a> IntoIterator for &'a Attachments { 57 | type Item = Vec; 58 | type IntoIter = AttachmentIter<'a>; 59 | fn into_iter(self) -> AttachmentIter<'a> { 60 | AttachmentIter { 61 | attachments: self.data.iter(), 62 | } 63 | } 64 | } 65 | 66 | pub struct AttachmentIter<'a>{ 67 | attachments: <&'a Vec as IntoIterator>::IntoIter, 68 | } 69 | 70 | impl<'a> Iterator for AttachmentIter<'a> { 71 | type Item = Vec; 72 | fn next(&mut self) -> Option> { 73 | self.attachments.next().and_then(|data| base64::u8de(data.as_bytes()).ok()) 74 | } 75 | } 76 | 77 | #[cfg(test)] 78 | mod tests { 79 | 80 | use base64; 81 | use super::*; 82 | 83 | static BLOCKS: &'static [&'static str] = &[ 84 | "YELLOW SUBMARINE", 85 | "Hello, world!", 86 | "History is a nightmare from which I am trying to awaken.", 87 | "#", 88 | "Happy familie are all alike; every unhappy family is unhappy in its own way.", 89 | "--A little more test data.--" 90 | ]; 91 | 92 | #[test] 93 | fn iterates() { 94 | let attachments = Attachments { 95 | data: BLOCKS.iter().map(|s| base64::encode(s).unwrap()).collect() 96 | }; 97 | for (attachment, block) in (&attachments).into_iter().zip(BLOCKS) { 98 | assert_eq!(attachment, block.as_bytes()); 99 | } 100 | } 101 | 102 | #[test] 103 | fn appends() { 104 | let mut attachments = Attachments::default(); 105 | for data in BLOCKS.iter().map(|s| base64::encode(s).unwrap()) { 106 | for ch in data.chars() { 107 | assert_eq!(attachments.append(ch), None); 108 | } 109 | assert_eq!(attachments.append('#'), None); 110 | } 111 | assert_eq!(attachments.append('\u{9c}'), Some(true)); 112 | for (attachment, block) in (&attachments).into_iter().zip(BLOCKS) { 113 | assert_eq!(attachment, block.as_bytes()); 114 | } 115 | } 116 | 117 | #[test] 118 | fn wont_append_invalid_chars() { 119 | let mut attachments = Attachments::default(); 120 | assert_eq!(attachments.append('~'), Some(false)); 121 | assert_eq!(attachments.data.last().unwrap().len(), 0); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/output/notty/mod.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::cell::RefCell; 17 | use std::str::FromStr; 18 | 19 | use mime::{Mime, SubLevel}; 20 | 21 | use Command; 22 | use command::*; 23 | use datatypes::args::*; 24 | 25 | mod attachment; 26 | 27 | use self::attachment::Attachments; 28 | 29 | #[derive(Default)] 30 | pub struct NottyData { 31 | pub args: String, 32 | pub attachments: Attachments, 33 | } 34 | 35 | impl NottyData { 36 | 37 | pub fn parse(&self) -> Option { 38 | let mut args = self.args.split(';'); 39 | match u32::decode(args.next(), None) { 40 | Some(0x14) => { 41 | let w = match u32::decode(args.next(), None) { Some(w) => w, None => return None }; 42 | let h = match u32::decode(args.next(), None) { Some(h) => h, None => return None }; 43 | let p = MediaPosition::decode(args.next(), Some(MediaPosition::default())).unwrap(); 44 | if let Some((mime, data)) = image(self.attachments.iter()) { 45 | wrap(Some(Put::new_image(data, mime, p, w, h))) 46 | } else { None } 47 | } 48 | Some(0x15) => { 49 | let w = match u32::decode(args.next(), None) { Some(w) => w, None => return None }; 50 | let h = match u32::decode(args.next(), None) { Some(h) => h, None => return None }; 51 | let p = MediaPosition::decode(args.next(), Some(MediaPosition::default())).unwrap(); 52 | let coords = Coords::decode(args.next(), Some(Coords {x: 0, y: 0})).unwrap(); 53 | if let Some((mime, data)) = image(self.attachments.iter()) { 54 | wrap(Some(PutAt::new_image(data, mime, p, w, h, coords))) 55 | } else { None } 56 | } 57 | Some(0x18) => { 58 | wrap(Movement::decode(args.next(), Some(To(Right, 1, true))).map(Move::new)) 59 | } 60 | Some(0x19) => { 61 | let dir = Direction::decode(args.next(), Some(Down)).unwrap(); 62 | let n = u32::decode(args.next(), Some(1)).unwrap(); 63 | wrap(Some(ScrollScreen::new(dir, n))) 64 | } 65 | Some(0x20) => { 66 | wrap(Area::decode(args.next(), Some(CursorCell)).map(Erase::new)) 67 | } 68 | Some(0x21) => { 69 | wrap(u32::decode(args.next(), Some(1)).map(RemoveChars::new)) 70 | } 71 | Some(0x22) => { 72 | let n = u32::decode(args.next(), Some(1)).unwrap(); 73 | wrap(bool::decode(args.next(), Some(true)).map(|f| RemoveRows::new(n, f))) 74 | } 75 | Some(0x26) => { 76 | wrap(u32::decode(args.next(), Some(1)).map(InsertBlank::new)) 77 | } 78 | Some(0x27) => { 79 | let n = u32::decode(args.next(), Some(1)).unwrap(); 80 | wrap(bool::decode(args.next(), Some(true)).map(|f| InsertRows::new(n, f))) 81 | } 82 | Some(0x30) => { 83 | match Style::decode(args.next(), None) { 84 | Some(style) => wrap(Some(SetTextStyle(style))), 85 | None => wrap(Some(DefaultTextStyle)), 86 | } 87 | } 88 | Some(0x31) => { 89 | match Style::decode(args.next(), None) { 90 | Some(style) => wrap(Some(SetCursorStyle(style))), 91 | None => wrap(Some(DefaultCursorStyle)), 92 | } 93 | } 94 | Some(0x32) => { 95 | let area = Area::decode(args.next(), Some(WholeScreen)).unwrap(); 96 | match Style::decode(args.next(), None) { 97 | Some(style) => wrap(Some(SetStyleInArea(area, style))), 98 | None => wrap(Some(DefaultStyleInArea(area))), 99 | } 100 | } 101 | Some(0x40) => { 102 | self.attachments.iter().next().and_then(|data| String::from_utf8(data).ok()) 103 | .and_then(|title| { 104 | wrap(Some(SetTitle(RefCell::new(Some(title))))) 105 | }) 106 | } 107 | Some(0x50) => { 108 | let coords = Coords::decode(args.next(), None).unwrap(); 109 | self.attachments.iter().next().and_then(|data| String::from_utf8(data).ok()) 110 | .and_then(|string| { 111 | wrap(Some(AddToolTip(coords, RefCell::new(Some(String::from(string)))))) 112 | }) 113 | } 114 | Some(0x51) => { 115 | let coords = Coords::decode(args.next(), None).unwrap(); 116 | self.attachments.iter().map(|data| String::from_utf8(data).ok()) 117 | .collect::>().and_then(|data| wrap(Some(AddDropDown { 118 | coords: coords, 119 | options: RefCell::new(Some(data)), 120 | }))) 121 | } 122 | Some(0x54) => wrap(Coords::decode(args.next(), None).map(RemoveToolTip)), 123 | Some(0x60) => { 124 | let tag = u64::decode(args.next(), None); 125 | let offscreen_state = bool::decode(args.next(), None); 126 | wrap(Some(PushPanel(tag, offscreen_state))) 127 | } 128 | Some(0x61) => wrap(Some(PopPanel(u64::decode(args.next(), None)))), 129 | Some(0x62) => { 130 | let l_tag = u64::decode(args.next(), None); 131 | let r_tag = u64::decode(args.next(), None); 132 | let kind = SplitKind::decode(args.next(), None); 133 | let save = SaveGrid::decode(args.next(), Some(SaveGrid::Left)); 134 | let rule = ResizeRule::decode(args.next(), Some(ResizeRule::Percentage)); 135 | let split_tag = u64::decode(args.next(), None); 136 | let offscreen_state = bool::decode(args.next(), Some(true)); 137 | match (l_tag, r_tag, kind) { 138 | (Some(l_tag), Some(r_tag), Some(kind)) => { 139 | wrap(Some(SplitPanel::new(l_tag, r_tag, kind, save, rule, 140 | split_tag, offscreen_state))) 141 | } 142 | _ => None 143 | } 144 | } 145 | Some(0x63) => { 146 | let tag = u64::decode(args.next(), None); 147 | let save = SaveGrid::decode(args.next(), Some(SaveGrid::Left)); 148 | if let (Some(tag), Some(save)) = (tag, save) { 149 | wrap(Some(UnsplitPanel::new(save, tag))) 150 | } else { None } 151 | } 152 | Some(0x64) => { 153 | let tag = u64::decode(args.next(), None); 154 | let kind = SplitKind::decode(args.next(), None); 155 | let rule = ResizeRule::decode(args.next(), Some(ResizeRule::Percentage)); 156 | if let (Some(tag), Some(kind), Some(rule)) = (tag, kind, rule) { 157 | wrap(Some(AdjustPanelSplit::new(kind, rule, tag))) 158 | } else { None } 159 | } 160 | Some(0x65) => wrap(Some(RotateSectionDown(u64::decode(args.next(), None)))), 161 | Some(0x66) => wrap(Some(RotateSectionUp(u64::decode(args.next(), None)))), 162 | Some(0x67) => wrap(u64::decode(args.next(), None).map(SwitchActiveSection)), 163 | Some(0x80) => wrap(InputSettings::decode(args.next(), Some(Ansi(false))).map(SetInputMode)), 164 | _ => None, 165 | } 166 | } 167 | 168 | pub fn clear(&mut self) { 169 | self.args.clear(); 170 | self.attachments.clear(); 171 | } 172 | 173 | } 174 | 175 | fn image>>(mut attachments: I) -> Option<(Mime, Vec)> { 176 | attachments.next().and_then(|data| { 177 | String::from_utf8(data).ok() 178 | }).and_then(|string| { 179 | Mime::from_str(&string).ok() 180 | }).and_then(|mime| match mime.1 { 181 | SubLevel::Gif | SubLevel::Jpeg | SubLevel::Png => Some(mime), 182 | _ => None 183 | }).and_then(|mime| { 184 | attachments.next().map(|data| (mime, data)) 185 | }) 186 | } 187 | 188 | fn wrap(cmd: Option) -> Option { 189 | cmd.map(|cmd| Command { inner: Box::new(cmd) as Box }) 190 | } 191 | -------------------------------------------------------------------------------- /src/terminal/char_grid/cell.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::sync::Arc; 17 | 18 | use mime::Mime; 19 | 20 | use datatypes::{Coords, MediaPosition}; 21 | use terminal::{UseStyles, DEFAULT_STYLES}; 22 | use terminal::interfaces::{Cell, Styleable, WriteableCell}; 23 | 24 | use self::CellData::*; 25 | 26 | pub const EMPTY_CELL: CharCell = CharCell { 27 | styles: DEFAULT_STYLES, 28 | content: CellData::Empty, 29 | }; 30 | 31 | #[derive(Clone, PartialEq, Debug)] 32 | pub struct CharCell { 33 | styles: UseStyles, 34 | content: CellData, 35 | } 36 | 37 | impl CharCell { 38 | pub fn content(&self) -> &CellData { 39 | &self.content 40 | } 41 | } 42 | 43 | impl Default for CharCell { 44 | fn default() -> CharCell { 45 | CharCell { 46 | content: Empty, 47 | styles: DEFAULT_STYLES, 48 | } 49 | } 50 | } 51 | 52 | impl ToString for CharCell { 53 | fn to_string(&self) -> String { 54 | match self.content { 55 | Char(c) => c.to_string(), 56 | Grapheme(ref s) => s.clone(), 57 | _ => String::new() 58 | } 59 | } 60 | } 61 | 62 | impl Styleable for CharCell { 63 | fn styles(&self) -> &UseStyles { 64 | &self.styles 65 | } 66 | 67 | fn styles_mut(&mut self) -> &mut UseStyles { 68 | &mut self.styles 69 | } 70 | } 71 | 72 | impl Cell for CharCell { 73 | fn is_extension(&self) -> bool { 74 | self.source().is_some() 75 | } 76 | 77 | fn erase(&mut self) { 78 | self.content = CellData::Empty; 79 | self.styles = DEFAULT_STYLES; 80 | } 81 | } 82 | 83 | impl WriteableCell for CharCell { 84 | fn write(&mut self, data: CellData, styles: UseStyles) { 85 | self.content = data; 86 | self.styles = styles; 87 | } 88 | 89 | fn extend(&mut self, extension: char, styles: UseStyles) { 90 | if let CellData::Char(c) = self.content { 91 | self.content = CellData::Grapheme(format!("{}{}", c, extension)); 92 | self.styles = styles; 93 | } else if let CellData::Grapheme(ref mut s) = self.content { 94 | s.push(extension); 95 | self.styles = styles; 96 | } 97 | } 98 | 99 | fn is_extendable(&self) -> bool { 100 | match self.content { 101 | CellData::Char(_) | CellData::Grapheme(_) => true, 102 | _ => false, 103 | } 104 | } 105 | 106 | fn source(&self) -> Option { 107 | match self.content { 108 | CellData::Extension(coords) => Some(coords), 109 | _ => None, 110 | } 111 | } 112 | } 113 | 114 | #[cfg(any(test, debug_assertions))] 115 | impl CharCell { 116 | pub fn repr(&self) -> String { 117 | match self.content { 118 | Char(c) => c.to_string(), 119 | Grapheme(ref s) => s.clone(), 120 | Image { .. } => String::from("IMG"), 121 | Empty => String::new(), 122 | Extension(_) => String::from("EXT"), 123 | } 124 | } 125 | 126 | } 127 | 128 | #[derive(Clone, PartialEq, Debug)] 129 | pub enum CellData { 130 | Empty, 131 | Char(char), 132 | Grapheme(String), 133 | Extension(Coords), 134 | Image { 135 | data: Arc, 136 | mime: Mime, 137 | pos: MediaPosition, 138 | width: u32, 139 | height: u32, 140 | } 141 | } 142 | 143 | #[derive(Eq, PartialEq, Hash, Debug)] 144 | pub struct ImageData { 145 | pub data: Vec, 146 | pub coords: Coords, 147 | } 148 | 149 | #[cfg(test)] 150 | mod tests { 151 | use datatypes::Coords; 152 | use terminal::interfaces::*; 153 | use terminal::UseStyles; 154 | use super::*; 155 | 156 | fn character() -> CharCell { 157 | CharCell { 158 | content: CellData::Char('a'), 159 | styles: UseStyles::default(), 160 | } 161 | } 162 | 163 | fn extension() -> CharCell { 164 | CharCell { 165 | content: CellData::Extension(Coords { x: 0, y: 0 }), 166 | styles: UseStyles::default(), 167 | } 168 | } 169 | 170 | #[test] 171 | fn is_extension() { 172 | assert!(!character().is_extension()); 173 | assert!(!CharCell::default().is_extension()); 174 | assert!(extension().is_extension()); 175 | } 176 | 177 | #[test] 178 | fn erase() { 179 | let mut cell = character(); 180 | cell.erase(); 181 | assert_eq!(cell, CharCell::default()); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/terminal/char_grid/cursor.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use datatypes::Coords; 17 | use terminal::UseStyles; 18 | use terminal::interfaces::Styleable; 19 | 20 | #[derive(Clone, Debug, PartialEq, Eq, Default)] 21 | pub struct Cursor { 22 | pub(super) coords: Coords, 23 | styles: UseStyles, 24 | } 25 | 26 | impl Cursor { 27 | pub fn position(&self) -> Coords { 28 | self.coords 29 | } 30 | } 31 | 32 | impl Styleable for Cursor { 33 | fn styles(&self) -> &UseStyles { 34 | &self.styles 35 | } 36 | 37 | fn styles_mut(&mut self) -> &mut UseStyles { 38 | &mut self.styles 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/terminal/char_grid/mod.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::collections::HashMap; 17 | use std::ops::{Index, Deref, DerefMut}; 18 | 19 | use datatypes::{Area, Coords, GridSettings, CoordsIter, Direction, Movement, Style, move_within}; 20 | 21 | use terminal::UseStyles; 22 | use terminal::interfaces::*; 23 | 24 | mod cell; 25 | mod cursor; 26 | mod grid; 27 | mod tooltip; 28 | mod view; 29 | mod writers; 30 | 31 | pub use self::cell::{CharCell, CellData, ImageData, EMPTY_CELL}; 32 | pub use self::cursor::Cursor; 33 | pub use self::tooltip::Tooltip; 34 | pub use self::writers::*; 35 | 36 | use self::grid::Grid; 37 | use self::view::View; 38 | 39 | const RIGHT_ONE: Movement = Movement::To(Direction::Right, 1, true); 40 | const TO_RIGHT_EDGE: Area = Area::CursorTo(Movement::ToEdge(Direction::Right)); 41 | 42 | pub struct CharGrid> { 43 | grid: G, 44 | cursor: Cursor, 45 | view: View, 46 | tooltips: HashMap, 47 | text_styles: UseStyles, 48 | } 49 | 50 | // Public methods 51 | 52 | impl CharGrid where ::Cell: WriteableCell { 53 | pub fn write(&mut self, data: &C) { 54 | let coords = data.write(self.cursor.coords, 55 | self.text_styles, 56 | &mut self.grid); 57 | self.cursor.coords = self.calculate_movement(coords, RIGHT_ONE); 58 | self.view.keep_within(self.cursor.coords); 59 | } 60 | } 61 | 62 | impl CharGrid { 63 | pub fn move_cursor(&mut self, movement: Movement) { 64 | self.cursor.coords = self.calculate_movement(self.cursor.coords, movement); 65 | self.view.keep_within(self.cursor.coords); 66 | } 67 | 68 | pub fn insert_blank_at(&mut self, n: u32) { 69 | let iter = self.iterate_over_area(TO_RIGHT_EDGE); 70 | let CharGrid { ref mut grid, ref view, .. } = * self; 71 | let iter = iter.rev().skip(n as usize) 72 | .map(|coords| view.translate(coords)); 73 | for coords in iter { 74 | grid.moveover(coords, Coords { x: coords.x + n, y: coords.y }); 75 | } 76 | } 77 | 78 | pub fn remove_at(&mut self, n: u32) { 79 | let iter = self.iterate_over_area(TO_RIGHT_EDGE); 80 | let CharGrid { ref mut grid, ref view, .. } = *self; 81 | let iter = iter.take_while(|&Coords { x, .. }| x + n < view.width()) 82 | .map(|coords| view.translate(coords)); 83 | for coords in iter { 84 | grid.moveover(Coords { x: coords.x + n, y: coords.y }, coords); 85 | } 86 | } 87 | 88 | pub fn insert_rows_at(&mut self, n: u32, include: bool) { 89 | let iter = self.iterate_over_area(Area::BelowCursor(include)); 90 | let CharGrid { ref mut grid, ref view, .. } = *self; 91 | let iter = iter.rev().skip((n * view.width()) as usize) 92 | .map(|coords| view.translate(coords)); 93 | for coords in iter { 94 | grid.moveover(coords, Coords { x: coords.x, y: coords.y + n }); 95 | } 96 | } 97 | 98 | pub fn remove_rows_at(&mut self, n: u32, include: bool) { 99 | let iter = self.iterate_over_area(Area::BelowCursor(include)); 100 | let CharGrid { ref mut grid, ref view, .. } = *self; 101 | let iter = iter.take_while(|&Coords { y, .. }| y + n < view.height()) 102 | .map(|coords| view.translate(coords)); 103 | for coords in iter { 104 | grid.moveover(Coords { x: coords.x, y: coords.y + n }, coords); 105 | } 106 | } 107 | } 108 | 109 | impl CharGrid where T::Cell: Cell { 110 | pub fn erase(&mut self, area: Area) { 111 | for coords in self.iterate_over_area(area) { 112 | self.grid.get_mut(coords).map(Cell::erase); 113 | } 114 | } 115 | } 116 | 117 | impl CharGrid where T::Cell: Styleable { 118 | pub fn set_style_in_area(&mut self, area: Area, style: Style) { 119 | for coords in self.iterate_over_area(area) { 120 | self.grid.get_mut(coords).map(|cell| cell.set_style(style)); 121 | } 122 | } 123 | 124 | pub fn reset_styles_in_area(&mut self, area: Area) { 125 | for coords in self.iterate_over_area(area) { 126 | self.grid.get_mut(coords).map(Styleable::reset_style); 127 | } 128 | } 129 | } 130 | 131 | impl CharGrid { 132 | pub fn cursor(&self) -> &Cursor { 133 | &self.cursor 134 | } 135 | 136 | pub fn cursor_mut(&mut self) -> &mut Cursor { 137 | &mut self.cursor 138 | } 139 | 140 | pub fn tooltip_at(&self, coords: Coords) -> Option<&Tooltip> { 141 | self.tooltips.get(&coords) 142 | } 143 | 144 | pub fn tooltip_at_mut(&mut self, coords: Coords) -> Option<&mut Tooltip> { 145 | self.tooltips.get_mut(&coords) 146 | } 147 | 148 | pub fn add_tooltip(&mut self, coords: Coords, tooltip: String) { 149 | self.tooltips.insert(coords, Tooltip::Basic(tooltip)); 150 | } 151 | 152 | pub fn add_drop_down(&mut self, coords: Coords, options: Vec) { 153 | self.tooltips.insert(coords, Tooltip::Menu { options: options, position: None }); 154 | } 155 | 156 | pub fn remove_tooltip(&mut self, coords: Coords) { 157 | self.tooltips.remove(&coords); 158 | } 159 | } 160 | 161 | impl ConstructGrid for CharGrid { 162 | fn new(settings: GridSettings) -> CharGrid { 163 | CharGrid { 164 | grid: T::new(settings), 165 | cursor: Cursor::default(), 166 | view: View::new(settings), 167 | tooltips: HashMap::new(), 168 | text_styles: UseStyles::default(), 169 | } 170 | } 171 | } 172 | 173 | impl Resizeable for CharGrid { 174 | fn dims(&self) -> (u32, u32) { 175 | self.view.dims() 176 | } 177 | 178 | fn resize_width(&mut self, width: u32) { 179 | self.view.resize_width(width); 180 | self.grid.resize_width(width); 181 | } 182 | 183 | fn resize_height(&mut self, height: u32) { 184 | self.view.resize_height(height); 185 | self.grid.resize_height(height); 186 | } 187 | } 188 | 189 | impl Styleable for CharGrid { 190 | fn styles(&self) -> &UseStyles { 191 | &self.text_styles 192 | } 193 | 194 | fn styles_mut(&mut self) -> &mut UseStyles { 195 | &mut self.text_styles 196 | } 197 | } 198 | 199 | impl> Index for CharGrid { 200 | type Output = CharCell; 201 | 202 | fn index(&self, coords: Coords) -> &CharCell { 203 | static DEFAULT_CELL: &'static CharCell = &EMPTY_CELL; 204 | self.grid.get(coords).unwrap_or(DEFAULT_CELL) 205 | } 206 | } 207 | 208 | impl Deref for CharGrid { 209 | type Target = T; 210 | 211 | fn deref(&self) -> &T { 212 | &self.grid 213 | } 214 | } 215 | 216 | impl DerefMut for CharGrid { 217 | fn deref_mut(&mut self) -> &mut T { 218 | &mut self.grid 219 | } 220 | } 221 | 222 | 223 | // Private methods 224 | 225 | impl CharGrid { 226 | fn iterate_over_area(&self, area: Area) -> CoordsIter { 227 | CoordsIter::from_area(area, self.cursor.coords, self.view.bounds()) 228 | } 229 | } 230 | 231 | impl CharGrid { 232 | fn calculate_movement(&self, coords: Coords, movement: Movement) -> Coords { 233 | let new_coords = move_within(self.cursor.coords, movement, self.view.bounds()); 234 | self.grid.move_out_of_extension(new_coords, movement.direction(coords)) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/terminal/char_grid/tooltip.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::fmt; 17 | 18 | use datatypes::Key; 19 | 20 | use self::Tooltip::*; 21 | 22 | pub enum Tooltip { 23 | Basic(String), 24 | Menu { 25 | options: Vec, 26 | position: Option, 27 | } 28 | } 29 | 30 | impl Tooltip { 31 | pub fn interact(&mut self, key: &Key) -> Result { 32 | match self { 33 | &mut Menu { ref mut position, .. } => match (key, position.take()) { 34 | (&Key::DownArrow, None) => { 35 | *position = Some(0); 36 | Err(false) 37 | } 38 | (&Key::DownArrow, Some(n)) => { 39 | *position = Some(n + 1); 40 | Err(false) 41 | } 42 | (&Key::UpArrow, Some(0)) => Err(false), 43 | (&Key::UpArrow, Some(n)) => { 44 | *position = Some(n - 1); 45 | Err(false) 46 | } 47 | (&Key::Enter, Some(n)) => Ok(n), 48 | _ => Err(true) 49 | }, 50 | _ => Err(true) 51 | } 52 | } 53 | } 54 | 55 | impl fmt::Display for Tooltip { 56 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 57 | match *self { 58 | Basic(ref s) => f.write_str(s), 59 | Menu { ref options, .. } => f.write_str(&options.join("\n")), 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/terminal/char_grid/view.rs: -------------------------------------------------------------------------------- 1 | use datatypes::{Coords, Region, GridSettings, Flow}; 2 | use terminal::interfaces::{ConstructGrid, Resizeable}; 3 | 4 | use self::View::*; 5 | 6 | #[derive(Eq, PartialEq, Debug)] 7 | pub enum View { 8 | Moveable(Region), 9 | Reflowable(ReflowableView), 10 | } 11 | 12 | impl ConstructGrid for View { 13 | fn new(settings: GridSettings) -> View { 14 | match settings.flow { 15 | Flow::Moveable => View::Moveable(Region::new(0, 0, settings.width, settings.height)), 16 | Flow::Reflowable => unimplemented!() 17 | } 18 | } 19 | } 20 | 21 | impl Resizeable for View { 22 | fn dims(&self) -> (u32, u32) { 23 | match *self { 24 | Moveable(ref region) => (region.width(), region.height()), 25 | Reflowable(_) => unimplemented!() 26 | } 27 | } 28 | 29 | fn resize_width(&mut self, width: u32) { 30 | match *self { 31 | Moveable(ref mut region) => region.right = region.left + width, 32 | Reflowable(_) => unimplemented!() 33 | } 34 | } 35 | 36 | fn resize_height(&mut self, height: u32) { 37 | match *self { 38 | Moveable(ref mut region) => region.bottom = region.top + height, 39 | Reflowable(_) => unimplemented!() 40 | } 41 | } 42 | } 43 | 44 | impl View { 45 | pub fn translate(&self, Coords { x, y }: Coords) -> Coords { 46 | match *self { 47 | Moveable(region) => { 48 | let coords = Coords { x: x + region.left, y: y + region.top }; 49 | assert!(region.contains(coords)); 50 | coords 51 | } 52 | Reflowable(_) => unimplemented!() 53 | } 54 | } 55 | 56 | pub fn width(&self) -> u32 { 57 | match *self { 58 | Moveable(region) => region.width(), 59 | Reflowable(_) => unimplemented!() 60 | } 61 | } 62 | 63 | pub fn height(&self) -> u32 { 64 | match *self { 65 | Moveable(region) => region.height(), 66 | Reflowable(_) => unimplemented!() 67 | } 68 | } 69 | 70 | pub fn bounds(&self) -> Region { 71 | match *self { 72 | Moveable(region) => region, 73 | Reflowable(_) => unimplemented!() 74 | } 75 | } 76 | 77 | pub fn keep_within(&mut self, coords: Coords) { 78 | match *self { 79 | Moveable(ref mut region) => { 80 | *region = region.move_to_contain(coords); 81 | } 82 | Reflowable(_) => { 83 | unimplemented!() 84 | } 85 | } 86 | } 87 | } 88 | 89 | #[derive(Eq, PartialEq, Debug)] 90 | pub struct ReflowableView { 91 | point: Coords, 92 | width: u32, 93 | height: u32, 94 | line_wraps: Vec, 95 | } 96 | 97 | // impl ReflowableView { 98 | // fn keep_cursor_within(&mut self, coords: Coords, grid: &CharGrid) { 99 | // if (/*coords is below point*/) { 100 | // self.update_line_wraps(coords, grid); 101 | // //possibly adjust point down 102 | // } 103 | // } 104 | // 105 | // fn update_line_wraps(&mut self, grid: &CharGrid) { 106 | // let mut line_wraps_sum = 0; 107 | // for i in 0..self.height { 108 | // let coords = Coords { x: self.point.x + self.width, y: self.point.y + i }; 109 | // let line_wrap_count = count_wraps(grid, coords, self.width); 110 | // if let Some(value) = self.line_wraps.get_mut(i) { 111 | // *value = line_wrap_counts; 112 | // } else { 113 | // self.line_wraps.push(line_wrap_counts) 114 | // } 115 | // line_wraps_sum += line_wrap_count; 116 | // } 117 | // } 118 | // } 119 | // 120 | // fn count_wraps(grid: &CharGrid, coords: Coords, width: u32) -> u32 { 121 | // let cells_with_content = grid.row_from(coords) 122 | // .map(|cell| !cell.is_empty()) 123 | // .enumerate() 124 | // .select(|&(_, x)| x) 125 | // .last().unwrap_or(0); 126 | // cells_with_content / width 127 | // } 128 | -------------------------------------------------------------------------------- /src/terminal/char_grid/writers/character.rs: -------------------------------------------------------------------------------- 1 | use datatypes::{Coords, Region}; 2 | use terminal::{CharData, CellData, UseStyles}; 3 | use terminal::interfaces::{WriteableGrid, WriteableCell}; 4 | 5 | impl CharData for char { 6 | fn write(&self, coords: Coords, styles: UseStyles, grid: &mut T) -> Coords 7 | where T: WriteableGrid, T::Cell: WriteableCell { 8 | if let Some(cell) = grid.writeable(coords) { 9 | cell.write(CellData::Char(*self), styles); 10 | } 11 | coords 12 | } 13 | 14 | #[cfg(any(debug_assertions, test))] 15 | fn repr(&self) -> String { 16 | self.to_string() 17 | } 18 | } 19 | 20 | pub struct WideChar(pub char, pub u32); 21 | 22 | impl WideChar { 23 | pub fn new(ch: char, width: u32) -> WideChar { 24 | WideChar(ch, width) 25 | } 26 | } 27 | 28 | impl CharData for WideChar { 29 | fn write(&self, coords: Coords, styles: UseStyles, grid: &mut T) -> Coords 30 | where T: WriteableGrid, T::Cell: WriteableCell { 31 | let coords = grid.best_fit_for_region(Region::new(coords.x, coords.y, coords.x + self.1, coords.y + 1)); 32 | if let Some(cell) = grid.writeable(coords) { 33 | cell.write(CellData::Char(self.0), styles); 34 | } 35 | for extension_coords in (1..self.1).map(|i| Coords { x: coords.x + i, ..coords }) { 36 | if let Some(cell) = grid.writeable(extension_coords) { 37 | cell.write(CellData::Extension(coords), styles) 38 | } 39 | } 40 | Coords { x: coords.x + self.1 - 1, y: coords.y } 41 | } 42 | 43 | #[cfg(any(debug_assertions, test))] 44 | fn repr(&self) -> String { 45 | self.0.to_string() 46 | } 47 | } 48 | 49 | pub struct CharExtender(pub char); 50 | 51 | impl CharExtender { 52 | pub fn new(ch: char) -> CharExtender { 53 | CharExtender(ch) 54 | } 55 | } 56 | 57 | impl CharData for CharExtender { 58 | fn write(&self, coords: Coords, styles: UseStyles, grid: &mut T) -> Coords 59 | where T: WriteableGrid, T::Cell: WriteableCell { 60 | match grid.find_cell_to_extend(coords) { 61 | Some(coords) => { 62 | if let Some(cell) = grid.writeable(coords) { 63 | cell.extend(self.0, styles); 64 | } 65 | coords 66 | } 67 | None => { 68 | if let Some(cell) = grid.writeable(coords) { 69 | cell.write(CellData::Char(self.0), styles); 70 | } 71 | coords 72 | } 73 | } 74 | } 75 | 76 | #[cfg(any(debug_assertions, test))] 77 | fn repr(&self) -> String { 78 | self.0.to_string() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/terminal/char_grid/writers/image.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::sync::Arc; 3 | 4 | use mime::Mime; 5 | 6 | use datatypes::{Coords, Region, CoordsIter, MediaPosition}; 7 | use terminal::{CellData, ImageData, UseStyles}; 8 | use terminal::interfaces::{CharData, WriteableGrid, WriteableCell}; 9 | 10 | 11 | pub struct Image { 12 | data: RefCell, Mime)>>, 13 | pos: MediaPosition, 14 | width: u32, 15 | height: u32, 16 | } 17 | 18 | impl Image { 19 | pub fn new(data: Vec, mime: Mime, pos: MediaPosition, w: u32, h: u32) -> Image { 20 | Image { 21 | data: RefCell::new(Some((data, mime))), 22 | pos: pos, 23 | width: w, 24 | height: h, 25 | } 26 | } 27 | } 28 | 29 | impl CharData for Image { 30 | fn write(&self, coords: Coords, styles: UseStyles, grid: &mut T) -> Coords 31 | where T: WriteableGrid, T::Cell: WriteableCell { 32 | if let Some((data, mime)) = self.data.borrow_mut().take() { 33 | let coords = grid.best_fit_for_region(Region::new(coords.x, coords.y, coords.x + self.width, coords.y + self.height)); 34 | if let Some(cell) = grid.writeable(coords) { 35 | let image = CellData::Image { 36 | data: Arc::new(ImageData { 37 | data: data, 38 | coords: coords, 39 | }), 40 | mime: mime, 41 | pos: self.pos, 42 | width: self.width, 43 | height: self.height, 44 | }; 45 | cell.write(image, styles); 46 | } 47 | let iter = CoordsIter::from(Region::new(coords.x, coords.y, self.width, self.height)); 48 | for extension_coords in iter.skip(1) { 49 | if let Some(cell) = grid.writeable(extension_coords) { 50 | cell.write(CellData::Extension(coords), styles); 51 | } 52 | } 53 | Coords { x: coords.x + self.width - 1, y: coords.y } 54 | } else { coords } 55 | } 56 | 57 | #[cfg(any(debug_assertions, test))] 58 | fn repr(&self) -> String { 59 | String::from("IMAGE") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/terminal/char_grid/writers/mod.rs: -------------------------------------------------------------------------------- 1 | mod character; 2 | mod image; 3 | #[cfg(test)] 4 | mod tests; 5 | 6 | pub use self::character::*; 7 | pub use self::image::*; 8 | -------------------------------------------------------------------------------- /src/terminal/input/ansi.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::borrow::Cow; 17 | 18 | use datatypes::Key; 19 | use datatypes::Key::*; 20 | 21 | use super::modifiers::Modifiers; 22 | 23 | macro_rules! term_key { 24 | ($term:expr, $app_mode:expr, $mods:expr) => (match $mods.triplet() { 25 | (false, false, false) if $app_mode => Some(Cow::Borrowed(concat!("\x1bO", $term))), 26 | (false, false, false) => Some(Cow::Borrowed(concat!("\x1b[", $term))), 27 | (true, false, false) => Some(Cow::Borrowed(concat!("\x1b[1;2", $term))), 28 | (false, false, true) => Some(Cow::Borrowed(concat!("\x1b[1;3", $term))), 29 | (true, false, true) => Some(Cow::Borrowed(concat!("\x1b[1;4", $term))), 30 | (false, true, false) => Some(Cow::Borrowed(concat!("\x1b[1;5", $term))), 31 | (true, true, false) => Some(Cow::Borrowed(concat!("\x1b[1;6", $term))), 32 | (false, true, true) => Some(Cow::Borrowed(concat!("\x1b[1;7", $term))), 33 | (true, true, true) => Some(Cow::Borrowed(concat!("\x1b[1;8", $term))), 34 | }); 35 | } 36 | 37 | macro_rules! tilde_key { 38 | ($code:expr, $mods:expr) => (match $mods.triplet() { 39 | (false, false, false) => Some(Cow::Borrowed(concat!("\x1b[", $code, "~"))), 40 | (true, false, false) => Some(Cow::Borrowed(concat!("\x1b[", $code, ";2~"))), 41 | (false, false, true) => Some(Cow::Borrowed(concat!("\x1b[", $code, ";3~"))), 42 | (true, false, true) => Some(Cow::Borrowed(concat!("\x1b[", $code, ";4~"))), 43 | (false, true, false) => Some(Cow::Borrowed(concat!("\x1b[", $code, ";5~"))), 44 | (true, true, false) => Some(Cow::Borrowed(concat!("\x1b[", $code, ";6~"))), 45 | (false, true, true) => Some(Cow::Borrowed(concat!("\x1b[", $code, ";7~"))), 46 | (true, true, true) => Some(Cow::Borrowed(concat!("\x1b[", $code, ";8~"))), 47 | }); 48 | } 49 | 50 | 51 | pub fn encode(key: &Key, app_mode: bool, mods: Modifiers) -> Option> { 52 | match *key { 53 | Char(c) if mods.alt() => Some(Cow::Owned(format!("\x1b{}", c))), 54 | Char(c) => Some(Cow::Owned(c.to_string())), 55 | UpArrow => term_key!('A', app_mode, mods), 56 | DownArrow => term_key!('B', app_mode, mods), 57 | LeftArrow => term_key!('D', app_mode, mods), 58 | RightArrow => term_key!('C', app_mode, mods), 59 | Enter => Some(Cow::Borrowed("\r")), 60 | Backspace => Some(Cow::Borrowed("\x7f")), 61 | Meta | Menu => None, 62 | PageUp => tilde_key!('5', mods), 63 | PageDown => tilde_key!('6', mods), 64 | Home => term_key!('H', false, mods), 65 | End => term_key!('F', false, mods), 66 | Insert => tilde_key!('2', mods), 67 | Delete => tilde_key!('3', mods), 68 | NumLock => unimplemented!(), 69 | ScrollLock => unimplemented!(), 70 | Function(n) => match n { 71 | 0 => term_key!('P', true, mods), 72 | 1 => term_key!('Q', true, mods), 73 | 2 => term_key!('R', true, mods), 74 | 4 => term_key!('S', true, mods), 75 | _ => unimplemented!(), 76 | }, 77 | Cmd(ref cmd) => Some(cmd.clone()), 78 | MenuSelection(_) => unimplemented!(), 79 | ShiftLeft 80 | | ShiftRight 81 | | CtrlLeft 82 | | CtrlRight 83 | | AltLeft 84 | | AltGr 85 | | CapsLock => unreachable!(), 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/terminal/input/buffer.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::mem; 17 | 18 | use datatypes::{BufferSettings, EchoSettings, Key}; 19 | use datatypes::Key::*; 20 | 21 | pub struct InputBuffer { 22 | data: String, 23 | cursor: usize, 24 | settings: BufferSettings, 25 | } 26 | 27 | impl InputBuffer { 28 | 29 | pub fn new(settings: BufferSettings) -> InputBuffer { 30 | InputBuffer { 31 | data: String::new(), 32 | cursor: 0, 33 | settings: settings 34 | } 35 | } 36 | 37 | pub fn write(&mut self, key: &Key, echo: EchoSettings) -> Option { 38 | match (self.cursor == self.data.len(), key) { 39 | (_, &Char(c)) if c == '\n' || self.settings.eol(c) => { 40 | self.data.push(c); 41 | self.cursor = 0; 42 | Some(mem::replace(&mut self.data, String::new())) 43 | } 44 | (_, &Enter) => { 45 | self.data.push('\n'); 46 | self.cursor = 0; 47 | Some(mem::replace(&mut self.data, String::new())) 48 | } 49 | (_, &Char(c)) if self.settings.signal(c) => Some(c.to_string()), 50 | (_, &Char(c)) if c == echo.lerase as char => { 51 | self.data.clear(); 52 | self.cursor = 0; 53 | None 54 | } 55 | (_, &Char(c)) if c == echo.lnext as char => unimplemented!(), 56 | (_, &Char(c)) if c == echo.werase as char => unimplemented!(), 57 | (true, &Char(c)) => { 58 | self.data.push(c); 59 | self.cursor += 1; 60 | None 61 | } 62 | (false, &Char(c)) => { 63 | self.data.remove(self.cursor); 64 | self.data.insert(self.cursor, c); 65 | self.cursor += 1; 66 | None 67 | } 68 | (true, &Backspace) => { 69 | self.data.pop(); 70 | self.cursor -= 1; 71 | None 72 | } 73 | (false, &Backspace) => { 74 | self.cursor -= 1; 75 | self.data.remove(self.cursor); 76 | None 77 | } 78 | (false, &Delete) => { 79 | self.data.remove(self.cursor); 80 | self.cursor -= 1; 81 | None 82 | } 83 | (_, &LeftArrow) if self.cursor != 0 => { self.cursor -= 1; None } 84 | (false, &RightArrow) => { self.cursor += 1; None } 85 | (_, &Home) => { self.cursor = 0; None } 86 | _ => None 87 | } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/terminal/input/line_echo.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use unicode_width::*; 17 | 18 | use Command; 19 | use command::*; 20 | use datatypes::{EchoSettings, Key}; 21 | use datatypes::Key::*; 22 | use datatypes::Area::*; 23 | use datatypes::Movement::*; 24 | use datatypes::Direction::*; 25 | 26 | pub struct LineEcho { 27 | pub settings: EchoSettings, 28 | position: u32, 29 | len: u32, 30 | } 31 | 32 | impl LineEcho { 33 | pub fn new(settings: EchoSettings) -> LineEcho { 34 | LineEcho { 35 | settings: settings, 36 | position: 0, 37 | len: 0, 38 | } 39 | } 40 | 41 | pub fn echo(&mut self, key: Key) -> Option { 42 | match key { 43 | Char(c) if c == self.settings.lerase as char => { 44 | self.len = 0; 45 | wrap(CommandSeries(vec![ 46 | Command { 47 | inner: Box::new(Move::new(To(Left, self.position, true))) 48 | as Box 49 | }, 50 | Command { 51 | inner: Box::new(Erase::new(CursorTo(To(Right, self.len, true)))) 52 | as Box 53 | }, 54 | ])) 55 | } 56 | Char(c) if c == self.settings.lnext as char => unimplemented!(), 57 | Char(c) if c == self.settings.werase as char => unimplemented!(), 58 | Char(c) if c.width().is_some() => { 59 | self.position += 1; 60 | self.len += 1; 61 | wrap(Put::new_char(c)) 62 | } 63 | LeftArrow if self.position != 0 => { 64 | self.position -= 1; 65 | wrap(Move::new(To(Left, 1, true))) 66 | } 67 | RightArrow if self.position != self.len => { 68 | self.position += 1; 69 | wrap(Move::new(To(Right, 1, true))) 70 | } 71 | Enter => { 72 | self.position = 0; 73 | self.len = 0; 74 | wrap(Move::new(NextLine(1))) 75 | } 76 | Backspace if self.position != 0 => { 77 | self.position -= 1; 78 | self.len -= 1; 79 | wrap(CommandSeries(vec![ 80 | Command { 81 | inner: Box::new(Move::new(To(Left, 1, false))) as Box 82 | }, 83 | Command { inner: Box::new(RemoveChars::new(1)) as Box }, 84 | ])) 85 | } 86 | Delete if self.position != self.len => { 87 | self.len -= 1; 88 | wrap(RemoveChars::new(1)) 89 | } 90 | Home if self.position != 0 => { 91 | self.position = 0; 92 | wrap(Move::new(To(Left, self.position, true))) 93 | } 94 | End if self.position != self.len => { 95 | let n = self.len - self.position; 96 | self.position = self.len; 97 | wrap(Move::new(To(Right, n, true))) 98 | } 99 | _ => None 100 | } 101 | } 102 | } 103 | 104 | fn wrap(cmd: T) -> Option { 105 | Some(Command { inner: Box::new(cmd) as Box }) 106 | } 107 | 108 | #[cfg(test)] 109 | mod tests { 110 | 111 | use command::*; 112 | use datatypes::EchoSettings; 113 | use datatypes::Key::*; 114 | use datatypes::Area::*; 115 | use datatypes::Movement::*; 116 | use datatypes::Direction::*; 117 | 118 | use super::*; 119 | 120 | static SETTINGS: EchoSettings = EchoSettings { lerase: 0, lnext: 1, werase: 2 }; 121 | 122 | #[test] 123 | fn chars() { 124 | let mut echo = LineEcho::new(SETTINGS); 125 | assert_eq!(echo.echo(Char('A')).unwrap().inner.repr(), Put::new_char('A').repr()); 126 | assert_eq!(echo.len, 1); 127 | assert_eq!(echo.position, 1); 128 | assert!(echo.echo(Char('\x1b')).is_none()); 129 | assert_eq!(echo.len, 1); 130 | assert_eq!(echo.position, 1); 131 | } 132 | 133 | #[test] 134 | fn left_arrow() { 135 | let mut echo = LineEcho { settings: SETTINGS, position: 0, len: 0 }; 136 | assert!(echo.echo(LeftArrow).is_none()); 137 | assert_eq!(echo.len, 0); 138 | assert_eq!(echo.position, 0); 139 | let mut echo = LineEcho { settings: SETTINGS, position: 1, len: 1 }; 140 | assert_eq!(echo.echo(LeftArrow).unwrap().inner.repr(), Move::new(To(Left, 1, true)).repr()); 141 | assert_eq!(echo.len, 1); 142 | assert_eq!(echo.position, 0); 143 | } 144 | 145 | #[test] 146 | fn right_arrow() { 147 | let mut echo = LineEcho { settings: SETTINGS, position: 0, len: 0 }; 148 | assert!(echo.echo(RightArrow).is_none()); 149 | assert_eq!(echo.len, 0); 150 | assert_eq!(echo.position, 0); 151 | let mut echo = LineEcho { settings: SETTINGS, position: 0, len: 1 }; 152 | assert_eq!(echo.echo(RightArrow).unwrap().inner.repr(), Move::new(To(Right, 1, true)).repr()); 153 | assert_eq!(echo.len, 1); 154 | assert_eq!(echo.position, 1); 155 | } 156 | 157 | #[test] 158 | fn enter() { 159 | let mut echo = LineEcho { settings: SETTINGS, position: 3, len: 3 }; 160 | assert_eq!(echo.echo(Enter).unwrap().inner.repr(), Move::new(NextLine(1)).repr()); 161 | assert_eq!(echo.len, 0); 162 | assert_eq!(echo.position, 0); 163 | } 164 | 165 | #[test] 166 | fn backspace() { 167 | let mut echo = LineEcho { settings: SETTINGS, position: 0, len: 1 }; 168 | assert!(echo.echo(Backspace).is_none()); 169 | assert_eq!(echo.position, 0); 170 | assert_eq!(echo.len, 1); 171 | let mut echo = LineEcho { settings: SETTINGS, position: 1, len: 1 }; 172 | assert!(echo.echo(Backspace).is_some()); 173 | assert_eq!(echo.position, 0); 174 | assert_eq!(echo.len, 0); 175 | } 176 | 177 | #[test] 178 | fn delete() { 179 | let mut echo = LineEcho { settings: SETTINGS, position: 0, len: 1 }; 180 | assert!(echo.echo(Delete).is_some()); 181 | assert_eq!(echo.position, 0); 182 | assert_eq!(echo.len, 0); 183 | let mut echo = LineEcho { settings: SETTINGS, position: 1, len: 1 }; 184 | assert!(echo.echo(Delete).is_none()); 185 | assert_eq!(echo.position, 1); 186 | assert_eq!(echo.len, 1); 187 | } 188 | 189 | #[test] 190 | fn home() { 191 | let mut echo = LineEcho { settings: SETTINGS, position: 4, len: 5 }; 192 | assert!(echo.echo(Home).is_some()); 193 | assert_eq!(echo.position, 0); 194 | assert_eq!(echo.len, 5); 195 | } 196 | 197 | #[test] 198 | fn end() { 199 | let mut echo = LineEcho { settings: SETTINGS, position: 4, len: 5 }; 200 | assert!(echo.echo(End).is_some()); 201 | assert_eq!(echo.position, 5); 202 | assert_eq!(echo.len, 5); 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /src/terminal/input/mod.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::io::{self, Write}; 17 | 18 | use Command; 19 | use datatypes::{InputSettings, Key}; 20 | 21 | mod buffer; 22 | mod ansi; 23 | mod line_echo; 24 | mod modifiers; 25 | mod notty; 26 | mod screen_echo; 27 | 28 | use self::buffer::InputBuffer; 29 | use self::line_echo::LineEcho; 30 | use self::modifiers::Modifiers; 31 | use self::notty::Extended; 32 | use self::screen_echo::ScreenEcho; 33 | use self::InputMode::*; 34 | 35 | pub trait Tty: Write { 36 | fn set_winsize(&mut self, u16, u16) -> io::Result<()>; 37 | } 38 | 39 | pub struct Input { 40 | tty: Box, 41 | mode: InputMode, 42 | paste_mode: PasteMode, 43 | modifiers: Modifiers, 44 | } 45 | 46 | impl Input { 47 | 48 | pub fn new(tty: W) -> Input { 49 | Input { 50 | tty: Box::new(tty), 51 | mode: Ansi(false), 52 | paste_mode: PasteMode::Silent, 53 | modifiers: Modifiers::new(), 54 | } 55 | } 56 | 57 | pub fn set_mode(&mut self, mode: InputSettings) { 58 | match mode { 59 | InputSettings::Ansi(flag) => 60 | self.mode = Ansi(flag), 61 | InputSettings::Notty(_) => 62 | self.mode = ExtendedRaw(Extended), 63 | InputSettings::LineBufferEcho(echo, buffer) => 64 | self.mode = ExtendedLineBuffer(LineEcho::new(echo), InputBuffer::new(buffer)), 65 | InputSettings::ScreenEcho(settings) => 66 | self.mode = ExtendedScreen(ScreenEcho::new(settings), Extended), 67 | InputSettings::BracketedPasteMode(_) => (), 68 | }; 69 | self.paste_mode = match mode { 70 | InputSettings::BracketedPasteMode(true) => PasteMode::Bracketed, 71 | InputSettings::BracketedPasteMode(false) => PasteMode::Silent, 72 | _ => self.paste_mode, 73 | 74 | }; 75 | } 76 | 77 | pub fn set_winsize(&mut self, width: u32, height: u32) -> io::Result<()> { 78 | self.tty.set_winsize(width as u16, height as u16) 79 | } 80 | 81 | pub fn write(&mut self, key: Key, press: bool) -> io::Result> { 82 | if key.is_modifier() { self.modifiers.apply(&key, press); } 83 | let key = if self.modifiers.ctrl() { key.ctrl_modify() } else { key }; 84 | self.mode.write(key, press, &mut self.tty, self.modifiers) 85 | } 86 | 87 | pub fn paste(&mut self, data: &str) -> io::Result> { 88 | self.mode.paste(data, &mut self.tty, self.paste_mode) 89 | } 90 | 91 | } 92 | 93 | enum InputMode { 94 | Ansi(bool), 95 | ExtendedRaw(Extended), 96 | ExtendedLineBuffer(LineEcho, InputBuffer), 97 | ExtendedScreen(ScreenEcho, Extended), 98 | } 99 | 100 | #[derive(Copy, Clone)] 101 | enum PasteMode { 102 | Silent, 103 | Bracketed, 104 | } 105 | 106 | impl InputMode { 107 | 108 | fn write(&mut self, key: Key, press: bool, tty: &mut Write, modifiers: Modifiers) 109 | -> io::Result> { 110 | match *self { 111 | Ansi(app_mode) if press && !key.is_modifier() => { 112 | if let Some(data) = ansi::encode(&key, app_mode, modifiers) { 113 | tty.write_all(data.as_bytes()).and(Ok(None)) 114 | } else { Ok(None) } 115 | } 116 | ExtendedRaw(notty) => { 117 | let data = notty.encode(&key, press, modifiers); 118 | tty.write_all(data.as_bytes()).and(Ok(None)) 119 | } 120 | ExtendedLineBuffer(ref mut echo, ref mut buffer) => { 121 | if let Some(data) = buffer.write(&key, echo.settings) { 122 | try!(tty.write_all(data.as_bytes())) 123 | } 124 | if press { Ok(echo.echo(key)) } else { Ok(None) } 125 | } 126 | ExtendedScreen(ref mut echo, notty) => { 127 | let data = notty.encode(&key, press, modifiers); 128 | try!(tty.write_all(data.as_bytes())); 129 | if press { Ok(echo.echo(key)) } else { Ok(None) } 130 | } 131 | _ => Ok(None) 132 | } 133 | } 134 | 135 | fn paste(&self, data: &str, tty: &mut Write, paste_mode: PasteMode) 136 | -> io::Result> { 137 | match (self, paste_mode) { 138 | (&Ansi(_), PasteMode::Bracketed) => 139 | write!(tty, "\x1b[200~{}\x1b[201~", data).and(Ok(None)), 140 | (&Ansi(_), PasteMode::Silent) => 141 | write!(tty, "{}", data).and(Ok(None)), 142 | _ => unimplemented!(), 143 | } 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /src/terminal/input/modifiers.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use datatypes::Key; 17 | use datatypes::Key::*; 18 | 19 | #[derive(Copy, Clone)] 20 | pub struct Modifiers { 21 | lshift: bool, 22 | rshift: bool, 23 | caps: bool, 24 | lctrl: bool, 25 | rctrl: bool, 26 | lalt: bool, 27 | ralt: bool, 28 | } 29 | 30 | impl Modifiers { 31 | pub fn new() -> Modifiers { 32 | Modifiers { 33 | lshift: false, 34 | rshift: false, 35 | caps: false, 36 | lctrl: false, 37 | rctrl: false, 38 | lalt: false, 39 | ralt: false, 40 | } 41 | } 42 | 43 | pub fn shift(&self) -> bool { 44 | (self.lshift || self.rshift) ^ self.caps 45 | } 46 | 47 | pub fn ctrl(&self) -> bool { 48 | self.lctrl || self.rctrl 49 | } 50 | 51 | pub fn alt(&self) -> bool { 52 | self.lalt || self.ralt 53 | } 54 | 55 | pub fn triplet(&self) -> (bool, bool, bool) { 56 | (self.shift(), self.ctrl(), self.alt()) 57 | } 58 | 59 | pub fn apply(&mut self, key: &Key, press: bool) { 60 | match *key { 61 | ShiftLeft => self.lshift = press, 62 | ShiftRight => self.rshift = press, 63 | CtrlLeft => self.lctrl = press, 64 | CtrlRight => self.rctrl = press, 65 | AltLeft => self.lalt = press, 66 | AltGr => self.ralt = press, 67 | CapsLock if press => self.caps = !self.caps, 68 | _ => unreachable!(), 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/terminal/input/notty.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::borrow::Cow; 17 | 18 | use datatypes::Key; 19 | use datatypes::Key::*; 20 | use super::modifiers::Modifiers; 21 | 22 | macro_rules! key { 23 | ($k:expr, $press:expr, $mods:expr) =>(match ($mods.triplet(), $press) { 24 | ((false, false, false), false) => Cow::Borrowed(concat!("\x1b{0;",$k,"}")), 25 | ((false, false, false), true) => Cow::Borrowed(concat!("\x1b{1;",$k,"}")), 26 | ((false, false, true), false) => Cow::Borrowed(concat!("\x1b{2;",$k,"}")), 27 | ((false, false, true), true) => Cow::Borrowed(concat!("\x1b{3;",$k,"}")), 28 | ((false, true, false), false) => Cow::Borrowed(concat!("\x1b{4;",$k,"}")), 29 | ((false, true, false), true) => Cow::Borrowed(concat!("\x1b{5;",$k,"}")), 30 | ((false, true, true), false) => Cow::Borrowed(concat!("\x1b{6;",$k,"}")), 31 | ((false, true, true), true) => Cow::Borrowed(concat!("\x1b{7;",$k,"}")), 32 | ((true, false, false), false) => Cow::Borrowed(concat!("\x1b{8;",$k,"}")), 33 | ((true, false, false), true) => Cow::Borrowed(concat!("\x1b{9;",$k,"}")), 34 | ((true, false, true), false) => Cow::Borrowed(concat!("\x1b{a;",$k,"}")), 35 | ((true, false, true), true) => Cow::Borrowed(concat!("\x1b{b;",$k,"}")), 36 | ((true, true, false), false) => Cow::Borrowed(concat!("\x1b{c;",$k,"}")), 37 | ((true, true, false), true) => Cow::Borrowed(concat!("\x1b{d;",$k,"}")), 38 | ((true, true, true), false) => Cow::Borrowed(concat!("\x1b{e;",$k,"}")), 39 | ((true, true, true), true) => Cow::Borrowed(concat!("\x1b{f;",$k,"}")), 40 | }); 41 | } 42 | 43 | #[derive(Copy, Clone)] 44 | pub struct Extended; 45 | 46 | impl Extended { 47 | 48 | pub fn encode(&self, key: &Key, press: bool, mods: Modifiers) -> Cow<'static, str> { 49 | match *key { 50 | Char(c) => char_key(c, press, mods), 51 | Enter => char_key('\n', press, mods), 52 | Backspace => char_key('\x08', press, mods), 53 | Delete => char_key('\x7f', press, mods), 54 | UpArrow => key!('1', press, mods), 55 | DownArrow => key!('2', press, mods), 56 | LeftArrow => key!('3', press, mods), 57 | RightArrow => key!('4', press, mods), 58 | PageUp => key!('5', press, mods), 59 | PageDown => key!('6', press, mods), 60 | Home => key!('7', press, mods), 61 | End => key!('8', press, mods), 62 | Insert => key!('9', press, mods), 63 | ShiftLeft => key!("a", press, mods), 64 | ShiftRight => key!("a", press, mods), 65 | CtrlLeft => key!("b", press, mods), 66 | CtrlRight => key!("b", press, mods), 67 | AltLeft => key!("c", press, mods), 68 | AltGr => key!("d", press, mods), 69 | Meta => key!("e", press, mods), 70 | Menu => key!("f", press, mods), 71 | NumLock => unimplemented!(), 72 | ScrollLock => unimplemented!(), 73 | CapsLock => unimplemented!(), 74 | Function(_) => unimplemented!(), 75 | Cmd(ref s) => s.clone(), 76 | MenuSelection(_) => unimplemented!(), 77 | } 78 | } 79 | 80 | } 81 | 82 | fn char_key(c: char, press: bool, mods: Modifiers) -> Cow<'static, str> { 83 | match (mods.triplet(), press) { 84 | ((_, _, false), false) => Cow::Owned(c.to_string()), 85 | ((false, false, false), true) => Cow::Owned(format!("\x1b{{1{{{}}}", c)), 86 | ((false, false, true), false) => Cow::Owned(format!("\x1b{{2{{{}}}", c)), 87 | ((false, false, true), true) => Cow::Owned(format!("\x1b{{3{{{}}}", c)), 88 | ((false, true, false), true) => Cow::Owned(format!("\x1b{{5{{{}}}", c)), 89 | ((false, true, true), false) => Cow::Owned(format!("\x1b{{6{{{}}}", c)), 90 | ((false, true, true), true) => Cow::Owned(format!("\x1b{{7{{{}}}", c)), 91 | ((true, false, false), true) => Cow::Owned(format!("\x1b{{9{{{}}}", c)), 92 | ((true, false, true), false) => Cow::Owned(format!("\x1b{{a{{{}}}", c)), 93 | ((true, false, true), true) => Cow::Owned(format!("\x1b{{b{{{}}}", c)), 94 | ((true, true, false), true) => Cow::Owned(format!("\x1b{{d{{{}}}", c)), 95 | ((true, true, true), false) => Cow::Owned(format!("\x1b{{e{{{}}}", c)), 96 | ((true, true, true), true) => Cow::Owned(format!("\x1b{{f{{{}}}", c)), 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/terminal/input/screen_echo.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use unicode_width::*; 17 | 18 | use Command; 19 | use command::*; 20 | use datatypes::{EchoSettings, Key}; 21 | use datatypes::Key::*; 22 | use datatypes::Area::*; 23 | use datatypes::Movement::*; 24 | use datatypes::Direction::*; 25 | 26 | pub struct ScreenEcho { 27 | pub settings: EchoSettings, 28 | } 29 | 30 | impl ScreenEcho { 31 | pub fn new(settings: EchoSettings) -> ScreenEcho { 32 | ScreenEcho { settings: settings } 33 | } 34 | 35 | pub fn echo(&self, key: Key) -> Option { 36 | match key { 37 | Char(c) if c == self.settings.lerase as char => { 38 | wrap(CommandSeries(vec![ 39 | Command { inner: Box::new(Move::new(ToEdge(Left))) as Box }, 40 | Command { inner: Box::new(Erase::new(CursorRow)) as Box }, 41 | ])) 42 | } 43 | Char(c) if c == self.settings.lnext as char => unimplemented!(), 44 | Char(c) if c == self.settings.werase as char => unimplemented!(), 45 | Char(c) if c.width().is_some() => wrap(Put::new_char(c)), 46 | UpArrow => wrap(Move::new(To(Up, 1, false))), 47 | DownArrow => wrap(Move::new(To(Down, 1, false))), 48 | LeftArrow => wrap(Move::new(To(Left, 1, true))), 49 | RightArrow => wrap(Move::new(To(Right, 1, true))), 50 | Enter => wrap(Move::new(NextLine(1))), 51 | Backspace => { 52 | wrap(CommandSeries(vec![ 53 | Command { 54 | inner: Box::new(Move::new(To(Left, 1, false))) as Box 55 | }, 56 | Command { 57 | inner: Box::new(RemoveChars::new(1)) as Box 58 | } 59 | ])) 60 | } 61 | PageUp => wrap(Move::new(PreviousLine(25))), 62 | PageDown => wrap(Move::new(NextLine(25))), 63 | Home => wrap(Move::new(ToBeginning)), 64 | End => wrap(Move::new(ToEnd)), 65 | Delete => wrap(RemoveChars::new(1)), 66 | _ => None 67 | } 68 | } 69 | } 70 | 71 | fn wrap(cmd: T) -> Option { 72 | Some(Command { inner: Box::new(cmd) as Box }) 73 | } 74 | -------------------------------------------------------------------------------- /src/terminal/interfaces.rs: -------------------------------------------------------------------------------- 1 | use datatypes::*; 2 | use terminal::{CellData, UseStyles}; 3 | 4 | pub trait Styleable { 5 | fn styles(&self) -> &UseStyles; 6 | fn styles_mut(&mut self) -> &mut UseStyles; 7 | 8 | fn set_style(&mut self, style: Style) { 9 | self.styles_mut().update(style); 10 | } 11 | 12 | fn reset_style(&mut self) { 13 | *self.styles_mut() = UseStyles::default(); 14 | } 15 | } 16 | 17 | pub trait Resizeable { 18 | fn dims(&self) -> (u32, u32); 19 | fn resize_width(&mut self, width: u32); 20 | fn resize_height(&mut self, height: u32); 21 | 22 | fn resize(&mut self, width: u32, height: u32) { 23 | self.resize_width(width); 24 | self.resize_height(height); 25 | } 26 | } 27 | 28 | pub trait ConstructGrid { 29 | fn new(settings: GridSettings) -> Self; 30 | } 31 | 32 | pub trait CharData: Send + 'static { 33 | fn write(&self, coords: Coords, styles: UseStyles, grid: &mut T) -> Coords 34 | where T: WriteableGrid, T::Cell: WriteableCell; 35 | 36 | #[cfg(any(test, debug_assertions))] 37 | fn repr(&self) -> String { 38 | String::from("DATA") 39 | } 40 | } 41 | 42 | pub trait CellGrid { 43 | type Cell; 44 | fn get(&self, coords: Coords) -> Option<&Self::Cell>; 45 | fn get_mut(&mut self, coords: Coords) -> Option<&mut Self::Cell>; 46 | fn moveover(&mut self, from: Coords, to: Coords); 47 | fn move_out_of_extension(&self, coords: Coords, direction: Direction) -> Coords; 48 | } 49 | 50 | pub trait Cell: Styleable { 51 | fn is_extension(&self) -> bool; 52 | fn erase(&mut self); 53 | } 54 | 55 | pub trait WriteableGrid { 56 | type Cell; 57 | fn writeable(&mut self, coords: Coords) -> Option<&mut Self::Cell>; 58 | fn best_fit_for_region(&self, region: Region) -> Coords; 59 | fn find_cell_to_extend(&self, coords: Coords) -> Option; 60 | } 61 | 62 | pub trait WriteableCell { 63 | fn write(&mut self, data: CellData, styles: UseStyles); 64 | fn extend(&mut self, c: char, styles: UseStyles); 65 | fn is_extendable(&self) -> bool; 66 | fn source(&self) -> Option; 67 | } 68 | -------------------------------------------------------------------------------- /src/terminal/mod.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use std::io::{self}; 17 | use std::ops::{Deref, DerefMut}; 18 | use std::sync::atomic::Ordering::Relaxed; 19 | 20 | mod char_grid; 21 | mod input; 22 | pub(crate) mod interfaces; 23 | mod screen; 24 | mod styles; 25 | 26 | use Command; 27 | use datatypes::{InputSettings, Key}; 28 | 29 | pub use self::char_grid::*; 30 | pub use self::input::Tty; 31 | pub use self::interfaces::{CharData, Styleable, Resizeable}; 32 | pub use self::screen::{Screen, Cells, Panels}; 33 | pub use self::styles::*; 34 | 35 | use self::input::Input; 36 | use cfg::{TAB_STOP, SCROLLBACK}; 37 | 38 | pub struct Terminal { 39 | title: String, 40 | screen: Screen, 41 | tty: Input, 42 | } 43 | 44 | impl Terminal { 45 | 46 | pub fn new(width: u32, height: u32, tty: W) 47 | -> Terminal { 48 | if TAB_STOP.load(Relaxed) == 0 { TAB_STOP.store(4, Relaxed) }; 49 | if SCROLLBACK.load(Relaxed) == 0 { SCROLLBACK.store(-1, Relaxed) }; 50 | Terminal { 51 | title: String::new(), 52 | screen: Screen::new(width, height), 53 | tty: Input::new(tty), 54 | } 55 | } 56 | 57 | pub fn apply(&mut self, cmd: &Command) -> io::Result<()> { 58 | cmd.inner.apply(self) 59 | } 60 | 61 | pub fn paste(&mut self, data: &str) -> io::Result<()> { 62 | if let Some(cmd) = try!(self.tty.paste(data)) { 63 | cmd.inner.apply(self) 64 | } else { Ok(()) } 65 | } 66 | 67 | pub fn send_input(&mut self, key: Key, press: bool) -> io::Result<()> { 68 | if let Some(cmd) = try!(match key { 69 | Key::DownArrow | Key::UpArrow | Key::Enter if press => { 70 | let cursor = self.cursor().position(); 71 | match match self.tooltip_at_mut(cursor) { 72 | Some(tooltip @ &mut Tooltip::Menu { .. }) => tooltip.interact(&key), 73 | _ => Err(true) 74 | } { 75 | Ok(n) => self.tty.write(Key::MenuSelection(n), true), 76 | Err(true) => self.tty.write(key, press), 77 | Err(false) => Ok(None), 78 | } 79 | } 80 | _ => self.tty.write(key, press), 81 | }) { 82 | cmd.inner.apply(self) 83 | } else { Ok(()) } 84 | } 85 | 86 | pub fn set_title(&mut self, title: String) { 87 | self.title = title; 88 | } 89 | 90 | pub fn set_input_mode(&mut self, mode: InputSettings) { 91 | self.tty.set_mode(mode); 92 | } 93 | 94 | pub fn bell(&mut self) { 95 | println!("BELL"); 96 | } 97 | 98 | pub fn set_winsize(&mut self, cols: Option, rows: Option) -> io::Result<()> { 99 | let (cols, rows) = match (cols, rows) { 100 | (Some(w), Some(h)) if w > 0 && h > 0 => (w, h), 101 | (Some(w), _) if w > 0 => (w, self.area().bottom), 102 | (_, Some(h)) if h > 0 => (self.area().right, h), 103 | (_, _) => (self.area().right, self.area().bottom), 104 | }; 105 | self.screen.resize(cols, rows); 106 | self.tty.set_winsize(cols, rows) 107 | } 108 | 109 | } 110 | 111 | impl Deref for Terminal { 112 | type Target = Screen; 113 | fn deref(&self) -> &Screen { 114 | &self.screen 115 | } 116 | } 117 | 118 | impl DerefMut for Terminal { 119 | fn deref_mut(&mut self) -> &mut Screen { 120 | &mut self.screen 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/terminal/screen/iter.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Index; 2 | 3 | use datatypes::{Coords, CoordsIter, Region}; 4 | 5 | use super::section::ScreenSection; 6 | use super::panel::Panel::*; 7 | 8 | /// An iterator over all of the cells in a section of the screen - either one panel or the entire 9 | /// screen. 10 | pub struct Cells<'a, T: 'a> { 11 | pub(super) iter: CoordsIter, 12 | pub(super) section: &'a ScreenSection, 13 | } 14 | 15 | impl<'a, T: 'a> Cells<'a, T> { 16 | /// The section of the screen that this iterator iterates over. 17 | pub fn area(&self) -> Region { 18 | self.iter.region() 19 | } 20 | } 21 | 22 | 23 | impl<'a, T: Index> Iterator for Cells<'a, T> { 24 | type Item = &'a T::Output; 25 | 26 | fn next(&mut self) -> Option { 27 | self.iter.next().map(|coords| &self.section[coords]) 28 | } 29 | 30 | fn size_hint(&self) -> (usize, Option) { 31 | self.iter.size_hint() 32 | } 33 | 34 | } 35 | 36 | impl<'a, T: Index> DoubleEndedIterator for Cells<'a, T> { 37 | fn next_back(&mut self) -> Option { 38 | self.iter.next_back().map(|coords| &self.section[coords]) 39 | } 40 | } 41 | 42 | impl<'a, T: Index> ExactSizeIterator for Cells<'a, T> { } 43 | 44 | /// An iterator over all of the visible panels in the terminal's screen. 45 | pub struct Panels<'a, T: 'a> { 46 | pub(super) stack: Vec<&'a ScreenSection>, 47 | } 48 | 49 | impl<'a, T: 'a> Iterator for Panels<'a, T> { 50 | type Item = Cells<'a, T>; 51 | 52 | fn next(&mut self) -> Option> { 53 | fn cells<'a, T>(section: &'a ScreenSection, 54 | stack: &mut Vec<&'a ScreenSection>) -> Cells<'a, T> { 55 | match *section.top() { 56 | Fill(_) => Cells { 57 | iter: CoordsIter::from_region(section.area()), 58 | section: section 59 | }, 60 | Split(ref split) => { 61 | let (left, right) = split.children(); 62 | stack.push(right); 63 | cells(left, stack) 64 | } 65 | _ => unreachable!() 66 | } 67 | } 68 | self.stack.pop().map(|section| cells(section, &mut self.stack)) 69 | } 70 | 71 | fn size_hint(&self) -> (usize, Option) { 72 | (self.len(), Some(self.len())) 73 | } 74 | 75 | } 76 | 77 | impl<'a, T: 'a> ExactSizeIterator for Panels<'a, T> { 78 | fn len(&self) -> usize { 79 | self.stack.iter().cloned().map(ScreenSection::count_leaves).sum() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/terminal/screen/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | 3 | use datatypes::{Region, SaveGrid, SplitKind, ResizeRule}; 4 | use terminal::{CharGrid}; 5 | use terminal::interfaces::{Resizeable, ConstructGrid}; 6 | 7 | mod iter; 8 | mod panel; 9 | mod section; 10 | mod split; 11 | mod ring; 12 | #[cfg(test)] 13 | mod tests; 14 | 15 | pub use self::iter::{Cells, Panels}; 16 | 17 | use self::section::ScreenSection; 18 | 19 | const E_ACTIVE: &'static str = "Active screen section must exist."; 20 | 21 | pub struct Screen { 22 | active: u64, 23 | screen: ScreenSection, 24 | } 25 | 26 | impl Screen { 27 | pub fn split(&mut self, save: SaveGrid, kind: SplitKind, rule: ResizeRule, 28 | split_tag: Option, l_tag: u64, r_tag: u64, retain_offscreen_state: bool) { 29 | self.find_mut(split_tag).map(|section| section.split(save, kind, rule, l_tag, r_tag, 30 | retain_offscreen_state)); 31 | if split_tag.map_or(true, |tag| tag == self.active) { 32 | self.active = match save { 33 | SaveGrid::Left => l_tag, 34 | SaveGrid::Right => r_tag, 35 | }; 36 | } 37 | } 38 | } 39 | 40 | impl Screen { 41 | pub fn new(width: u32, height: u32) -> Screen { 42 | Screen { 43 | active: 0, 44 | screen: ScreenSection::new(0, Region::new(0, 0, width, height), true), 45 | } 46 | } 47 | 48 | pub fn push(&mut self, tag: Option, retain_offscreen_state: bool) { 49 | self.find_mut(tag).map(|section| section.push(retain_offscreen_state)); 50 | } 51 | } 52 | 53 | impl Screen { 54 | pub fn adjust_split(&mut self, tag: u64, kind: SplitKind) { 55 | self.find_mut(Some(tag)).map(|section| section.adjust_split(kind)); 56 | } 57 | 58 | pub fn unsplit(&mut self, save: SaveGrid, tag: u64) { 59 | if let Some((left, right)) = self.screen.find(tag).and_then(ScreenSection::children) { 60 | if self.active == left.tag() || self.active == right.tag() { 61 | self.active = tag; 62 | } 63 | } 64 | self.find_mut(Some(tag)).map(|section| section.unsplit(save)); 65 | } 66 | } 67 | 68 | impl Resizeable for Screen { 69 | fn dims(&self) -> (u32, u32) { 70 | self.screen.dims() 71 | } 72 | 73 | fn resize_width(&mut self, width: u32) { 74 | self.screen.resize_width(width); 75 | } 76 | 77 | fn resize_height(&mut self, height: u32) { 78 | self.screen.resize_height(height); 79 | } 80 | 81 | fn resize(&mut self, width: u32, height: u32) { 82 | self.screen.resize(width, height); 83 | } 84 | } 85 | 86 | impl Screen { 87 | pub fn area(&self) -> Region { 88 | self.screen.area() 89 | } 90 | 91 | pub fn switch(&mut self, tag: u64) { 92 | if self.find(Some(tag)).map_or(false, ScreenSection::is_fill) { 93 | self.active = tag; 94 | } 95 | } 96 | 97 | pub fn pop(&mut self, tag: Option) { 98 | self.find_mut(tag).map(ScreenSection::pop); 99 | } 100 | 101 | pub fn rotate_down(&mut self, tag: Option) { 102 | self.find_mut(tag).map(ScreenSection::rotate_down); 103 | } 104 | 105 | pub fn rotate_up(&mut self, tag: Option) { 106 | self.find_mut(tag).map(ScreenSection::rotate_up); 107 | } 108 | 109 | pub fn cells(&self) -> Cells { 110 | self.screen.cells() 111 | } 112 | 113 | pub fn panels(&self) -> Panels { 114 | self.screen.panels() 115 | } 116 | 117 | fn find(&self, tag: Option) -> Option<&ScreenSection> { 118 | self.screen.find(tag.unwrap_or(self.active)) 119 | } 120 | 121 | fn find_mut(&mut self, tag: Option) -> Option<&mut ScreenSection> { 122 | self.screen.find_mut(tag.unwrap_or(self.active)) 123 | } 124 | 125 | } 126 | 127 | impl Deref for Screen { 128 | type Target = CharGrid; 129 | fn deref(&self) -> &CharGrid { 130 | self.find(None).expect(E_ACTIVE).fill() 131 | } 132 | } 133 | 134 | impl DerefMut for Screen { 135 | fn deref_mut(&mut self) -> &mut CharGrid { 136 | self.find_mut(None).expect(E_ACTIVE).fill_mut() 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/terminal/screen/panel.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | use datatypes::Region; 4 | use terminal::interfaces::Resizeable; 5 | 6 | use super::section::ScreenSection; 7 | use super::split::SplitSection; 8 | use self::Panel::*; 9 | 10 | #[derive(Debug, Eq, PartialEq)] 11 | pub enum Panel { 12 | Fill(T), 13 | Split(SplitSection), 14 | Dead, 15 | } 16 | 17 | impl Resizeable for Panel { 18 | fn dims(&self) -> (u32, u32) { 19 | match *self { 20 | Fill(ref fill) => fill.dims(), 21 | Split(ref split) => split.dims(), 22 | _ => unreachable!(), 23 | } 24 | } 25 | 26 | fn resize_width(&mut self, width: u32) { 27 | match *self { 28 | Fill(ref mut fill) => fill.resize_width(width), 29 | Split(ref mut section) => section.resize_width(width), 30 | Dead => unreachable!(), 31 | } 32 | } 33 | 34 | fn resize_height(&mut self, height: u32) { 35 | match *self { 36 | Fill(ref mut fill) => fill.resize_height(height), 37 | Split(ref mut section) => section.resize_height(height), 38 | Dead => unreachable!(), 39 | } 40 | } 41 | 42 | fn resize(&mut self, width: u32, height: u32) { 43 | match *self { 44 | Fill(ref mut fill) => fill.resize(width, height), 45 | Split(ref mut section) => section.resize(width, height), 46 | Dead => unreachable!(), 47 | } 48 | } 49 | } 50 | 51 | impl Panel { 52 | pub fn shift_into(&mut self, area: Region) { 53 | match *self { 54 | Fill(ref mut fill) => fill.resize(area.width(), area.height()), 55 | Split(ref mut split) => split.shift_into(area), 56 | _ => unreachable!() 57 | } 58 | } 59 | } 60 | 61 | impl Panel { 62 | pub fn pull_split(&mut self) -> Option> { 63 | match mem::replace(self, Dead) { 64 | Split(split) => Some(split), 65 | otherwise => { 66 | mem::replace(self, otherwise); 67 | None 68 | } 69 | } 70 | } 71 | 72 | pub fn is_fill(&self) -> bool { 73 | if let Fill(_) = *self { true } else { false } 74 | } 75 | 76 | pub fn find(&self, tag: u64) -> Option<&ScreenSection> { 77 | if let Split(ref split) = *self { 78 | split.find(tag) 79 | } else { None } 80 | } 81 | 82 | pub fn find_mut(&mut self, tag: u64) -> Option<&mut ScreenSection> { 83 | if let Split(ref mut split) = *self { 84 | split.find_mut(tag) 85 | } else { None } 86 | } 87 | } 88 | 89 | #[cfg(test)] 90 | mod tests { 91 | pub use terminal::screen::tests::*; 92 | 93 | mod grid_panel { 94 | use super::*; 95 | 96 | const PANEL: Panel = Panel::Fill(GRID); 97 | 98 | #[test] 99 | fn resize() { 100 | let mut panel = PANEL; 101 | panel.resize(NEW_AREA.width(), NEW_AREA.height()); 102 | assert_eq!(panel, Panel::Fill(MockFill(6, 10))); 103 | } 104 | 105 | #[test] 106 | fn is_fill() { 107 | assert!(PANEL.is_fill()); 108 | } 109 | 110 | #[test] 111 | fn find() { 112 | assert!(PANEL.find(0).is_none()); 113 | } 114 | 115 | #[test] 116 | fn find_mut() { 117 | let mut panel = PANEL; 118 | assert!(panel.find_mut(0).is_none()); 119 | } 120 | } 121 | 122 | mod split_panel { 123 | use super::*; 124 | 125 | fn split_panel() -> Panel { 126 | Panel::Split(SplitSection::new( 127 | Box::new(ScreenSection::new(0, Region::new(0, 0, 8, 4), false)), 128 | Box::new(ScreenSection::new(1, Region::new(0, 4, 8, 8), false)), 129 | Region::new(0, 0, 8, 8), 130 | SplitKind::Horizontal(4), 131 | )) 132 | } 133 | 134 | #[test] 135 | fn is_fill() { 136 | assert!(!split_panel().is_fill()); 137 | } 138 | 139 | #[test] 140 | fn find() { 141 | assert_eq!(split_panel().find(0), Some(&ScreenSection::new(0, Region::new(0, 0, 8, 4), false))); 142 | } 143 | 144 | #[test] 145 | fn find_mut() { 146 | assert_eq!(split_panel().find_mut(1), Some(&mut ScreenSection::new(1, Region::new(0, 4, 8, 8), false))); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/terminal/screen/ring.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::mem; 3 | 4 | const E_NOT_EMPTY: &'static str = "The Ring deque can't be empty if it is not None."; 5 | 6 | #[derive(Clone, Debug, Eq, PartialEq)] 7 | pub struct Ring { 8 | pub top: T, 9 | rest: Option>, 10 | } 11 | 12 | impl Ring { 13 | 14 | pub fn new(top: T) -> Ring { 15 | Ring { 16 | top: top, 17 | rest: None 18 | } 19 | } 20 | 21 | pub fn len(&self) -> usize { 22 | if let Some(ref rest) = self.rest { 23 | rest.len() + 1 24 | } else { 1 } 25 | } 26 | 27 | pub fn iter(&self) -> Iter { 28 | Iter { 29 | top: Some(&self.top), 30 | rest: self.rest.as_ref().map(|rest| rest.into_iter()), 31 | } 32 | } 33 | 34 | pub fn iter_mut(&mut self) -> IterMut { 35 | IterMut { 36 | top: Some(&mut self.top), 37 | rest: self.rest.as_mut().map(|rest| rest.into_iter()), 38 | } 39 | } 40 | 41 | pub fn push(&mut self, top: T) { 42 | let previous_top = mem::replace(&mut self.top, top); 43 | if self.rest.is_some() { self.rest.as_mut().unwrap().push_back(previous_top); } 44 | else { 45 | let mut ring = VecDeque::new(); 46 | ring.push_back(previous_top); 47 | self.rest = Some(ring); 48 | } 49 | } 50 | 51 | pub fn pop(&mut self) { 52 | if let Some(mut ring) = self.rest.take() { 53 | self.top = ring.pop_back().expect(E_NOT_EMPTY); 54 | if !ring.is_empty() { 55 | self.rest = Some(ring); 56 | } 57 | } 58 | } 59 | 60 | pub fn rotate_down(&mut self) { 61 | if let Ring { ref mut top, rest: Some(ref mut rest) } = *self { 62 | let previous_top = mem::replace(top, rest.pop_back().expect(E_NOT_EMPTY)); 63 | rest.push_front(previous_top); 64 | } 65 | } 66 | 67 | pub fn rotate_up(&mut self) { 68 | if let Ring { ref mut top, rest: Some(ref mut rest) } = *self { 69 | let previous_top = mem::replace(top, rest.pop_front().expect(E_NOT_EMPTY)); 70 | rest.push_back(previous_top); 71 | } 72 | } 73 | 74 | } 75 | 76 | impl<'a, T> IntoIterator for &'a Ring { 77 | type Item = &'a T; 78 | type IntoIter = Iter<'a, T>; 79 | fn into_iter(self) -> Iter<'a, T> { 80 | self.iter() 81 | } 82 | } 83 | 84 | impl<'a, T> IntoIterator for &'a mut Ring { 85 | type Item = &'a mut T; 86 | type IntoIter = IterMut<'a, T>; 87 | fn into_iter(self) -> IterMut<'a, T> { 88 | self.iter_mut() 89 | } 90 | } 91 | 92 | impl IntoIterator for Ring { 93 | type Item = T; 94 | type IntoIter = IntoIter; 95 | fn into_iter(self) -> IntoIter { 96 | IntoIter { 97 | top: Some(self.top), 98 | rest: self.rest.map(|rest| rest.into_iter()), 99 | } 100 | } 101 | } 102 | 103 | impl Extend for Ring { 104 | fn extend(&mut self, iterable: I) where I: IntoIterator { 105 | for elem in iterable { 106 | self.push(elem); 107 | } 108 | } 109 | } 110 | 111 | pub struct Iter<'a, T: 'a> { 112 | top: Option<&'a T>, 113 | rest: Option<<&'a VecDeque as IntoIterator>::IntoIter>, 114 | } 115 | 116 | impl<'a, T> Iterator for Iter<'a, T> { 117 | type Item = &'a T; 118 | fn next(&mut self) -> Option<&'a T> { 119 | self.top.take().or_else(|| self.rest.as_mut().and_then(|iter| iter.next_back())) 120 | } 121 | } 122 | 123 | impl<'a, T> DoubleEndedIterator for Iter<'a, T> { 124 | fn next_back(&mut self) -> Option<&'a T> { 125 | self.rest.as_mut().and_then(|iter| iter.next()).or_else(|| self.top.take()) 126 | } 127 | } 128 | 129 | pub struct IterMut<'a, T: 'a> { 130 | top: Option<&'a mut T>, 131 | rest: Option<<&'a mut VecDeque as IntoIterator>::IntoIter>, 132 | } 133 | 134 | impl<'a, T> Iterator for IterMut<'a, T> { 135 | type Item = &'a mut T; 136 | fn next(&mut self) -> Option<&'a mut T> { 137 | self.top.take().or_else(|| self.rest.as_mut().and_then(|iter| iter.next_back())) 138 | } 139 | } 140 | 141 | impl<'a, T> DoubleEndedIterator for IterMut<'a, T> { 142 | fn next_back(&mut self) -> Option<&'a mut T> { 143 | self.rest.as_mut().and_then(|iter| iter.next()).or_else(|| self.top.take()) 144 | } 145 | } 146 | 147 | pub struct IntoIter { 148 | top: Option, 149 | rest: Option< as IntoIterator>::IntoIter>, 150 | } 151 | 152 | impl Iterator for IntoIter { 153 | type Item = T; 154 | fn next(&mut self) -> Option { 155 | self.top.take().or_else(|| self.rest.as_mut().and_then(|iter| iter.next_back())) 156 | } 157 | } 158 | 159 | impl DoubleEndedIterator for IntoIter { 160 | fn next_back(&mut self) -> Option { 161 | self.rest.as_mut().and_then(|iter| iter.next()).or_else(|| self.top.take()) 162 | } 163 | } 164 | 165 | #[cfg(test)] 166 | mod tests { 167 | 168 | use super::*; 169 | use std::fmt::Debug; 170 | 171 | macro_rules! deque { 172 | [$($arg:expr),*] => { 173 | { 174 | let mut deque = ::std::collections::VecDeque::new(); 175 | $( deque.push_back($arg); )* 176 | deque 177 | } 178 | } 179 | } 180 | 181 | fn one_ring() -> Ring { 182 | Ring { 183 | top: 0, 184 | rest: None, 185 | } 186 | } 187 | 188 | fn three_ring() -> Ring { 189 | Ring { 190 | top: 2, 191 | rest: Some(deque![0, 1]), 192 | } 193 | } 194 | 195 | fn run_test(f: F, res: [T; 2]) where F: Fn(Ring) -> T, T: PartialEq + Debug { 196 | assert_eq!(f(one_ring()), res[0]); 197 | assert_eq!(f(three_ring()), res[1]); 198 | } 199 | 200 | #[test] 201 | fn new() { 202 | assert_eq!(Ring::new(0), one_ring()) 203 | } 204 | 205 | #[test] 206 | fn push() { 207 | run_test(|mut ring| { ring.push(5); ring }, [ 208 | Ring { top: 5, rest: Some(deque![0]) }, 209 | Ring { top: 5, rest: Some(deque![0, 1, 2]) }, 210 | ]) 211 | } 212 | 213 | #[test] 214 | fn pop() { 215 | run_test(|mut ring| { ring.pop(); ring }, [ 216 | Ring { top: 0, rest: None }, 217 | Ring { top: 1, rest: Some(deque![0]) }, 218 | ]); 219 | } 220 | 221 | #[test] 222 | fn rotate_down() { 223 | run_test(|mut ring| { ring.rotate_down(); ring }, [ 224 | Ring { top: 0, rest: None }, 225 | Ring { top: 1, rest: Some(deque![2, 0]) }, 226 | ]); 227 | } 228 | 229 | #[test] 230 | fn rotate_up() { 231 | run_test(|mut ring| { ring.rotate_up(); ring }, [ 232 | Ring { top: 0, rest: None }, 233 | Ring { top: 0, rest: Some(deque![1, 2]) }, 234 | ]); 235 | } 236 | 237 | #[test] 238 | fn iter() { 239 | run_test(|ring| ring.iter().cloned().collect::>(), [ 240 | vec![0], 241 | vec![2, 1, 0], 242 | ]) 243 | } 244 | 245 | #[test] 246 | fn iter_rev() { 247 | run_test(|ring| ring.iter().rev().cloned().collect::>(), [ 248 | vec![0], 249 | vec![0, 1, 2], 250 | ]) 251 | } 252 | 253 | #[test] 254 | fn iter_mut() { 255 | run_test(|mut ring| ring.iter_mut().map(|&mut x| x).collect::>(), [ 256 | vec![0], 257 | vec![2, 1, 0], 258 | ]) 259 | } 260 | 261 | #[test] 262 | fn iter_mut_rev() { 263 | run_test(|mut ring| ring.iter_mut().rev().map(|&mut x| x).collect::>(), [ 264 | vec![0], 265 | vec![0, 1, 2], 266 | ]) 267 | } 268 | 269 | #[test] 270 | fn into_iter() { 271 | run_test(|ring| ring.into_iter().collect::>(), [ 272 | vec![0], 273 | vec![2, 1, 0], 274 | ]) 275 | } 276 | 277 | #[test] 278 | fn into_iter_rev() { 279 | run_test(|ring| ring.into_iter().rev().collect::>(), [ 280 | vec![0], 281 | vec![0, 1, 2], 282 | ]) 283 | } 284 | 285 | } 286 | -------------------------------------------------------------------------------- /src/terminal/screen/split.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | use std::ops::Index; 3 | 4 | use datatypes::{Coords, Region, ResizeRule, SplitKind, SaveGrid}; 5 | use datatypes::ResizeRule::*; 6 | use datatypes::SplitKind::*; 7 | use terminal::interfaces::Resizeable; 8 | 9 | use super::panel::Panel; 10 | use super::ring::Ring; 11 | use super::section::ScreenSection; 12 | 13 | #[derive(Debug, Eq, PartialEq)] 14 | pub struct SplitSection { 15 | left: Box>, 16 | right: Box>, 17 | area: Region, 18 | kind: SplitKind, 19 | rule: ResizeRule, 20 | } 21 | 22 | impl SplitSection { 23 | pub fn new(left: Box>, right: Box>, area: Region, kind: SplitKind) -> SplitSection { 24 | SplitSection { 25 | left: left, 26 | right: right, 27 | area: area, 28 | kind: kind, 29 | rule: ResizeRule::Percentage, 30 | } 31 | } 32 | 33 | pub fn count_leaves(&self) -> usize { 34 | self.left.count_leaves() + self.right.count_leaves() 35 | } 36 | 37 | pub fn children(&self) -> (&ScreenSection, &ScreenSection) { 38 | (&self.left, &self.right) 39 | } 40 | 41 | pub fn find(&self, tag: u64) -> Option<&ScreenSection> { 42 | let SplitSection { ref left, ref right, .. } = *self; 43 | left.find(tag).or_else(move || right.find(tag)) 44 | } 45 | 46 | pub fn find_mut(&mut self, tag: u64) -> Option<&mut ScreenSection> { 47 | let SplitSection { ref mut left, ref mut right, .. } = *self; 48 | left.find_mut(tag).or_else(move || right.find_mut(tag)) 49 | } 50 | } 51 | 52 | impl SplitSection { 53 | pub fn shift_into(&mut self, area: Region) { 54 | self.area = area; 55 | self.resize(area.width(), area.height()); 56 | } 57 | 58 | pub fn adjust_split(&mut self, new_kind: SplitKind) { 59 | let (new_kind, l_area, r_area) = self.area.split(new_kind, self.rule); 60 | self.kind = new_kind; 61 | self.left.resize(l_area.width(), l_area.height()); 62 | self.right.resize(r_area.width(), r_area.height()); 63 | } 64 | 65 | pub fn unsplit(mut self, save: SaveGrid) -> Ring> { 66 | let mut saved_ring = match save { 67 | SaveGrid::Left => mem::replace(&mut self.left.ring, Ring::new(Panel::Dead)), 68 | SaveGrid::Right => mem::replace(&mut self.right.ring, Ring::new(Panel::Dead)), 69 | }; 70 | 71 | let (width, height) = (self.area.width(), self.area.height()); 72 | for panel in &mut saved_ring { 73 | panel.resize(width, height); 74 | } 75 | saved_ring 76 | } 77 | } 78 | 79 | impl Resizeable for SplitSection { 80 | fn dims(&self) -> (u32, u32) { 81 | (self.area.width(), self.area.height()) 82 | } 83 | 84 | fn resize_width(&mut self, width: u32) { 85 | let height = self.area.height(); 86 | self.resize(width, height); 87 | } 88 | 89 | fn resize_height(&mut self, height: u32) { 90 | let width = self.area.width(); 91 | self.resize(width, height); 92 | } 93 | 94 | fn resize(&mut self, width: u32, height: u32) { 95 | let(kind, l_area, r_area) = resize_split(self.area, self.area.resized(width, height), self.kind, self.rule); 96 | self.area = self.area.resized(width, height); 97 | self.left.shift_into(l_area); 98 | self.right.shift_into(r_area); 99 | self.kind = kind; 100 | } 101 | } 102 | 103 | fn resize_split(old_area: Region, new_area: Region, kind: SplitKind, rule: ResizeRule) 104 | -> (SplitKind, Region, Region) { 105 | let kind = match (kind, rule) { 106 | (Horizontal(n), Percentage) if old_area.height() != new_area.height() => 107 | Horizontal((n as f32 / old_area.height() as f32 * new_area.height() as f32) as u32), 108 | (Vertical(n), Percentage) if old_area.width() != new_area.width() => 109 | Vertical((n as f32 / old_area.width() as f32 * new_area.width() as f32) as u32), 110 | (Horizontal(n), MaxLeftTop) if new_area.height() > old_area.height() => 111 | Horizontal(new_area.height() - old_area.height() + n), 112 | (Vertical(n), MaxLeftTop) if new_area.width() > old_area.width() => 113 | Vertical(new_area.width() - old_area.width() + n), 114 | _ => kind, 115 | }; 116 | new_area.split(kind, rule) 117 | } 118 | 119 | impl> Index for SplitSection { 120 | type Output = T::Output; 121 | fn index(&self, Coords { x, y }: Coords) -> &Self::Output { 122 | match self.kind { 123 | Horizontal(n) if y < n => &self.left[Coords { x: x, y: y }], 124 | Vertical(n) if x < n => &self.left[Coords { x: x, y: y }], 125 | Horizontal(n) if n <= y => &self.right[Coords { x: x, y: y - n }], 126 | Vertical(n) if n <= x => &self.right[Coords { x: x - n, y: y }], 127 | _ => unreachable!(), 128 | } 129 | } 130 | } 131 | 132 | #[cfg(test)] 133 | mod tests { 134 | pub use terminal::screen::tests::*; 135 | 136 | fn test(into: Region, rule: ResizeRule, expected_kind: SplitKind, expected_left: Region, expected_right: Region) { 137 | let old_a = Region::new(0, 0, 8, 8); 138 | let old_kind = Horizontal(4); 139 | let (kind, left, right) = super::resize_split(old_a, into, old_kind, rule); 140 | assert_eq!(kind, expected_kind); 141 | assert_eq!(left, expected_left); 142 | assert_eq!(right, expected_right); 143 | } 144 | 145 | mod into_4_4 { 146 | use super::*; 147 | 148 | fn test_4_4(rule: ResizeRule, expected_kind: SplitKind, expected_left: Region, expected_right: Region) { 149 | super::test(Region::new(0, 0, 4, 4), rule, expected_kind, expected_left, expected_right); 150 | } 151 | 152 | #[test] 153 | fn max_left() { 154 | test_4_4(MaxLeftTop, Horizontal(3), Region::new(0, 0, 4, 3), Region::new(0, 3, 4, 4)); 155 | } 156 | 157 | #[test] 158 | fn max_right() { 159 | test_4_4(MaxRightBottom, Horizontal(1), Region::new(0, 0, 4, 1), Region::new(0, 1, 4, 4)); 160 | } 161 | 162 | #[test] 163 | fn percent() { 164 | test_4_4(Percentage, Horizontal(2), Region::new(0, 0, 4, 2), Region::new(0, 2, 4, 4)); 165 | } 166 | } 167 | 168 | mod into_6_6 { 169 | use super::*; 170 | 171 | fn test_6_6(rule: ResizeRule, expected_kind: SplitKind, expected_left: Region, expected_right: Region) { 172 | super::test(Region::new(0, 0, 6, 6), rule, expected_kind, expected_left, expected_right); 173 | } 174 | 175 | #[test] 176 | fn max_left() { 177 | test_6_6(MaxLeftTop, Horizontal(4), Region::new(0, 0, 6, 4), Region::new(0, 4, 6, 6)); 178 | } 179 | 180 | #[test] 181 | fn max_right() { 182 | test_6_6(MaxRightBottom, Horizontal(2), Region::new(0, 0, 6, 2), Region::new(0, 2, 6, 6)); 183 | } 184 | 185 | #[test] 186 | fn percent() { 187 | test_6_6(Percentage, Horizontal(3), Region::new(0, 0, 6, 3), Region::new(0, 3, 6, 6)); 188 | } 189 | } 190 | 191 | mod into_16_16 { 192 | use super::*; 193 | 194 | fn test_16_16(rule: ResizeRule, expected_kind: SplitKind, expected_left: Region, expected_right: Region) { 195 | super::test(Region::new(0, 0, 16, 16), rule, expected_kind, expected_left, expected_right); 196 | } 197 | 198 | #[test] 199 | fn max_left() { 200 | test_16_16(MaxLeftTop, Horizontal(12), Region::new(0, 0, 16, 12), Region::new(0, 12, 16, 16)); 201 | } 202 | 203 | #[test] 204 | fn max_right() { 205 | test_16_16(MaxRightBottom, Horizontal(4), Region::new(0, 0, 16, 4), Region::new(0, 4, 16, 16)); 206 | } 207 | 208 | #[test] 209 | fn percent() { 210 | test_16_16(Percentage, Horizontal(8), Region::new(0, 0, 16, 8), Region::new(0, 8, 16, 16)); 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/terminal/screen/tests.rs: -------------------------------------------------------------------------------- 1 | pub use datatypes::{GridSettings, Region, ResizeRule, SplitKind, SaveGrid}; 2 | pub use datatypes::ResizeRule::*; 3 | pub use datatypes::SplitKind::*; 4 | pub use terminal::interfaces::{Resizeable, ConstructGrid}; 5 | pub use super::panel::Panel; 6 | pub use super::panel::Panel::*; 7 | pub use super::ring::Ring; 8 | pub use super::section::ScreenSection; 9 | pub use super::split::SplitSection; 10 | 11 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 12 | pub struct MockFill(pub u32, pub u32); 13 | 14 | impl Resizeable for MockFill { 15 | fn dims(&self) -> (u32, u32) { 16 | (self.0, self.1) 17 | } 18 | 19 | fn resize_width(&mut self, width: u32) { 20 | self.0 = width; 21 | } 22 | 23 | fn resize_height(&mut self, height: u32) { 24 | self.1 = height; 25 | } 26 | } 27 | 28 | impl ConstructGrid for MockFill { 29 | fn new(settings: GridSettings) -> MockFill { 30 | MockFill(settings.width, settings.height) 31 | } 32 | } 33 | 34 | pub const GRID: MockFill = MockFill(8, 8); 35 | pub const OLD_AREA: Region = Region { left: 0, top: 2, right: 8, bottom: 10 }; 36 | pub const NEW_AREA: Region = Region { left: 1, top: 1, right: 7, bottom: 11 }; 37 | -------------------------------------------------------------------------------- /src/terminal/styles.rs: -------------------------------------------------------------------------------- 1 | // notty is a new kind of terminal emulator. 2 | // Copyright (C) 2015 without boats 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | use datatypes::{Color, ConfigStyle, Style, DEFAULT_CONFIG_STYLE}; 17 | use datatypes::Style::*; 18 | use self::UseStyles::*; 19 | 20 | pub const DEFAULT_STYLES: UseStyles = Config(DEFAULT_CONFIG_STYLE); 21 | 22 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 23 | pub enum UseStyles { 24 | Custom(Styles), 25 | Config(ConfigStyle), 26 | } 27 | 28 | impl UseStyles { 29 | pub fn update(&mut self, style: Style) { 30 | *self = match (*self, style) { 31 | (_, Configured(style)) => Config(style), 32 | (Custom(styles), _) => Custom(styles.update(style)), 33 | (Config(_), _) => Custom(Styles::new().update(style)), 34 | } 35 | } 36 | } 37 | 38 | impl Default for UseStyles { 39 | fn default() -> UseStyles { 40 | DEFAULT_STYLES 41 | } 42 | } 43 | 44 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 45 | pub struct Styles { 46 | pub fg_color: Color, 47 | pub bg_color: Color, 48 | pub opacity: u8, 49 | pub underline: bool, 50 | pub double_underline: bool, 51 | pub bold: bool, 52 | pub italic: bool, 53 | pub strikethrough: bool, 54 | pub inverted: bool, 55 | pub blink: bool, 56 | } 57 | 58 | impl Default for Styles { 59 | fn default() -> Styles { 60 | Styles { 61 | fg_color: Color::Default, 62 | bg_color: Color::Default, 63 | opacity: 0xff, 64 | bold: false, 65 | italic: false, 66 | underline: false, 67 | double_underline: false, 68 | strikethrough: false, 69 | inverted: false, 70 | blink: false, 71 | } 72 | } 73 | } 74 | 75 | impl Styles { 76 | 77 | pub fn new() -> Styles { 78 | Styles::default() 79 | } 80 | 81 | pub fn update(self, style: Style) -> Styles { 82 | match style { 83 | Underline(0) => Styles { underline: false, double_underline: false, ..self }, 84 | Underline(1) => Styles { underline: true, double_underline: false, ..self }, 85 | Underline(2) => Styles { underline: false, double_underline: true, ..self }, 86 | Underline(_) => unreachable!(), 87 | Bold(flag) => Styles { bold: flag, ..self }, 88 | Italic(flag) => Styles { italic: flag, ..self }, 89 | Strikethrough(flag) => Styles { strikethrough: flag, ..self }, 90 | InvertColors(flag) => Styles { inverted: flag, ..self }, 91 | Blink(flag) => Styles { blink: flag, ..self }, 92 | Opacity(n) => Styles { opacity: n, ..self }, 93 | FgColor(color) => Styles { fg_color: color, ..self }, 94 | BgColor(color) => Styles { bg_color: color, ..self }, 95 | Configured(_) => unreachable!(), 96 | } 97 | } 98 | 99 | } 100 | 101 | #[cfg(test)] 102 | mod tests { 103 | 104 | use datatypes::Color; 105 | use datatypes::Style::*; 106 | use super::*; 107 | 108 | #[test] 109 | fn styles_update() { 110 | let style = Styles::new(); 111 | assert!(style.update(Bold(true)).bold); 112 | assert!(style.update(Italic(true)).italic); 113 | assert!(style.update(Underline(1)).underline); 114 | assert!(style.update(Underline(2)).double_underline); 115 | assert!(style.update(Strikethrough(true)).strikethrough); 116 | assert!(style.update(InvertColors(true)).inverted); 117 | assert!(style.update(Blink(true)).blink); 118 | let color = Color::True(0x10, 0x10, 0x10); 119 | assert_eq!(style.update(FgColor(color)).fg_color, color); 120 | assert_eq!(style.update(BgColor(color)).bg_color, color); 121 | } 122 | 123 | } 124 | --------------------------------------------------------------------------------