├── .gitignore ├── examples ├── term_size.rs ├── 256color.rs ├── get_keys.rs ├── custom-event.rs ├── hello-world.rs ├── 256color_on_screen.rs ├── split.rs ├── win.rs ├── stack.rs ├── true_color.rs └── termbox.rs ├── src ├── event.rs ├── prelude.rs ├── macros.rs ├── sys │ ├── size.rs │ ├── mod.rs │ ├── file.rs │ └── signal.rs ├── draw.rs ├── color.rs ├── cell.rs ├── lib.rs ├── widget │ ├── util.rs │ ├── mod.rs │ ├── align.rs │ ├── stack.rs │ └── win.rs ├── attr.rs ├── error.rs ├── spinlock.rs ├── canvas.rs ├── raw.rs ├── key.rs ├── screen.rs ├── output.rs ├── input.rs └── term.rs ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── ci.yml ├── Cargo.toml ├── LICENSE ├── CHANGELOG.md ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | .idea 4 | -------------------------------------------------------------------------------- /examples/term_size.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use tuikit::output::Output; 3 | 4 | fn main() { 5 | let output = Output::new(Box::new(io::stdout())).unwrap(); 6 | let (width, height) = output.terminal_size().unwrap(); 7 | println!("width: {}, height: {}", width, height); 8 | } 9 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | //! events a `Term` could return 2 | 3 | pub use crate::key::Key; 4 | 5 | #[derive(Eq, PartialEq, Hash, Debug, Copy, Clone)] 6 | pub enum Event { 7 | Key(Key), 8 | Resize { 9 | width: usize, 10 | height: usize, 11 | }, 12 | Restarted, 13 | /// user defined signal 1 14 | User(UserEvent), 15 | 16 | #[doc(hidden)] 17 | __Nonexhaustive, 18 | } 19 | -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use crate::attr::{Attr, Color, Effect}; 2 | pub use crate::canvas::Canvas; 3 | pub use crate::cell::Cell; 4 | pub use crate::draw::{Draw, DrawResult}; 5 | pub use crate::event::Event; 6 | pub use crate::key::*; 7 | pub use crate::term::{Term, TermHeight, TermOptions}; 8 | pub use crate::widget::{ 9 | AlignSelf, HSplit, HorizontalAlign, Rectangle, Size, Split, Stack, VSplit, VerticalAlign, 10 | Widget, Win, 11 | }; 12 | pub use crate::Result; 13 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! ok_or_return { 3 | ($expr:expr, $default_val:expr) => { 4 | match $expr { 5 | Ok(val) => val, 6 | Err(_) => { 7 | return $default_val; 8 | } 9 | } 10 | }; 11 | } 12 | 13 | #[macro_export] 14 | macro_rules! some_or_return { 15 | ($expr:expr, $default_val:expr) => { 16 | match $expr { 17 | Some(val) => val, 18 | None => { 19 | return $default_val; 20 | } 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/sys/size.rs: -------------------------------------------------------------------------------- 1 | use std::{io, mem}; 2 | 3 | use super::cvt; 4 | use nix::libc::{c_int, c_ushort, ioctl, TIOCGWINSZ}; 5 | 6 | #[repr(C)] 7 | struct TermSize { 8 | row: c_ushort, 9 | col: c_ushort, 10 | _x: c_ushort, 11 | _y: c_ushort, 12 | } 13 | 14 | /// Get the size of the terminal. 15 | pub fn terminal_size(fd: c_int) -> io::Result<(usize, usize)> { 16 | unsafe { 17 | let mut size: TermSize = mem::zeroed(); 18 | cvt(ioctl(fd, TIOCGWINSZ.into(), &mut size as *mut _))?; 19 | Ok((size.col as usize, size.row as usize)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /src/sys/mod.rs: -------------------------------------------------------------------------------- 1 | // copy from https://docs.rs/crate/termion/1.5.1/source/src/sys/unix/mod.rs 2 | use std::io; 3 | pub mod file; 4 | pub mod signal; 5 | pub mod size; 6 | 7 | trait IsMinusOne { 8 | fn is_minus_one(&self) -> bool; 9 | } 10 | 11 | macro_rules! impl_is_minus_one { 12 | ($($t:ident)*) => ($(impl IsMinusOne for $t { 13 | fn is_minus_one(&self) -> bool { 14 | *self == -1 15 | } 16 | })*) 17 | } 18 | 19 | impl_is_minus_one! { i8 i16 i32 i64 isize } 20 | 21 | fn cvt(t: T) -> io::Result { 22 | if t.is_minus_one() { 23 | Err(io::Error::last_os_error()) 24 | } else { 25 | Ok(t) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tuikit" 3 | version = "0.5.0" 4 | authors = ["Jinzhou Zhang "] 5 | description = "Toolkit for writing TUI applications" 6 | documentation = "https://docs.rs/tuikit" 7 | homepage = "https://github.com/lotabout/tuikit" 8 | repository = "https://github.com/lotabout/tuikit" 9 | readme = "README.md" 10 | keywords = ["tui", "terminal", "tty", "color"] 11 | license = "MIT" 12 | edition = "2018" 13 | 14 | [dependencies] 15 | lazy_static = "1.2.0" 16 | nix = { version = "0.24.1", default-features = false, features = ["fs", "poll", "signal", "term"] } 17 | bitflags = "1.0.4" 18 | term = "0.7" 19 | unicode-width = "0.1.5" 20 | log = "0.4" 21 | 22 | [dev-dependencies] 23 | env_logger = "0.6.1" 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release to crates.io 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 1 16 | - name: Install correct toolchain 17 | uses: actions-rs/toolchain@v1 18 | with: 19 | toolchain: stable 20 | override: true 21 | - name: Run cargo check 22 | uses: actions-rs/cargo@v1 23 | with: 24 | command: check 25 | - name: Run tests 26 | run: TERM=linux cargo test --verbose 27 | - name: Login crates.io 28 | run: cargo login ${CRATES_IO_TOKEN} 29 | env: 30 | CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} 31 | - run: cargo publish 32 | -------------------------------------------------------------------------------- /examples/256color.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use tuikit::attr::Color; 3 | use tuikit::output::Output; 4 | 5 | fn main() { 6 | let mut output = Output::new(Box::new(io::stdout())).unwrap(); 7 | 8 | for fg in 0..=255 { 9 | output.set_fg(Color::AnsiValue(fg)); 10 | output.write(format!("{:5}", fg).as_str()); 11 | if fg % 16 == 15 { 12 | output.reset_attributes(); 13 | output.write("\n"); 14 | output.flush() 15 | } 16 | } 17 | 18 | output.reset_attributes(); 19 | 20 | for bg in 0..=255 { 21 | output.set_bg(Color::AnsiValue(bg)); 22 | output.write(format!("{:5}", bg).as_str()); 23 | if bg % 16 == 15 { 24 | output.reset_attributes(); 25 | output.write("\n"); 26 | output.flush() 27 | } 28 | } 29 | 30 | output.flush() 31 | } 32 | -------------------------------------------------------------------------------- /examples/get_keys.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use tuikit::input::KeyBoard; 3 | use tuikit::key::Key; 4 | use tuikit::output::Output; 5 | use tuikit::raw::IntoRawMode; 6 | 7 | fn main() { 8 | let _stdout = std::io::stdout().into_raw_mode().unwrap(); 9 | let mut output = Output::new(Box::new(_stdout)).unwrap(); 10 | output.enable_mouse_support(); 11 | output.flush(); 12 | 13 | println!("program will exit on pressing `q` or wait 5 seconds"); 14 | 15 | // let mut keyboard = KeyBoard::new(Box::new(std::io::stdin())); 16 | let mut keyboard = KeyBoard::new_with_tty(); 17 | while let Ok(key) = keyboard.next_key_timeout(Duration::from_secs(5)) { 18 | if key == Key::Char('q') { 19 | break; 20 | } 21 | println!("print: {:?}", key); 22 | } 23 | output.disable_mouse_support(); 24 | output.flush(); 25 | } 26 | -------------------------------------------------------------------------------- /examples/custom-event.rs: -------------------------------------------------------------------------------- 1 | use bitflags::_core::result::Result::Ok; 2 | 3 | use tuikit::prelude::*; 4 | 5 | fn main() { 6 | let term: Term = 7 | Term::with_height(TermHeight::Percent(30)).expect("term creation error"); 8 | let _ = term.print(0, 0, "Press 'q' or 'Ctrl-c' to quit!"); 9 | while let Ok(ev) = term.poll_event() { 10 | match ev { 11 | Event::Key(Key::Char('q')) | Event::Key(Key::Ctrl('c')) => break, 12 | Event::Key(key) => { 13 | let _ = term.print(1, 0, format!("get key: {:?}", key).as_str()); 14 | let _ = term.send_event(Event::User(format!("key: {:?}", key))); 15 | } 16 | Event::User(ev_str) => { 17 | let _ = term.print(2, 0, format!("user event: {}", &ev_str).as_str()); 18 | } 19 | _ => { 20 | let _ = term.print(3, 0, format!("event: {:?}", ev).as_str()); 21 | } 22 | } 23 | let _ = term.present(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/sys/file.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use std::os::unix::io::RawFd; 3 | use std::time::Duration; 4 | 5 | use crate::error::TuikitError; 6 | use nix::sys::select; 7 | use nix::sys::time::{TimeVal, TimeValLike}; 8 | 9 | fn duration_to_timeval(duration: Duration) -> TimeVal { 10 | let sec = duration.as_secs() * 1000 + (duration.subsec_millis() as u64); 11 | TimeVal::milliseconds(sec as i64) 12 | } 13 | 14 | pub fn wait_until_ready(fd: RawFd, signal_fd: Option, timeout: Duration) -> Result<()> { 15 | let mut timeout_spec = if timeout == Duration::new(0, 0) { 16 | None 17 | } else { 18 | Some(duration_to_timeval(timeout)) 19 | }; 20 | 21 | let mut fdset = select::FdSet::new(); 22 | fdset.insert(fd); 23 | signal_fd.map(|fd| fdset.insert(fd)); 24 | let n = select::select(None, &mut fdset, None, None, &mut timeout_spec)?; 25 | 26 | if n < 1 { 27 | Err(TuikitError::Timeout(timeout)) // this error message will be used in input.rs 28 | } else if fdset.contains(fd) { 29 | Ok(()) 30 | } else { 31 | Err(TuikitError::Interrupted) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jinzhou Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/hello-world.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::{max, min}; 2 | use tuikit::prelude::*; 3 | 4 | fn main() { 5 | let term: Term<()> = Term::with_height(TermHeight::Percent(30)).unwrap(); 6 | let mut row = 1; 7 | let mut col = 0; 8 | 9 | let _ = term.print(0, 0, "press arrow key to move the text, (q) to quit"); 10 | let _ = term.present(); 11 | 12 | while let Ok(ev) = term.poll_event() { 13 | let _ = term.clear(); 14 | let _ = term.print(0, 0, "press arrow key to move the text, (q) to quit"); 15 | 16 | let (width, height) = term.term_size().unwrap(); 17 | match ev { 18 | Event::Key(Key::ESC) | Event::Key(Key::Char('q')) | Event::Key(Key::Ctrl('c')) => break, 19 | Event::Key(Key::Up) => row = max(row - 1, 1), 20 | Event::Key(Key::Down) => row = min(row + 1, height - 1), 21 | Event::Key(Key::Left) => col = max(col, 1) - 1, 22 | Event::Key(Key::Right) => col = min(col + 1, width - 1), 23 | _ => {} 24 | } 25 | 26 | let _ = term.print_with_attr(row, col, "Hello World! 你好!今日は。", Color::RED); 27 | let _ = term.set_cursor(row, col); 28 | let _ = term.present(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | test: 11 | name: test 12 | runs-on: ${{matrix.os}} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macOS-latest] 16 | rust: [stable] 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 1 22 | - name: Install correct toolchain 23 | uses: actions-rs/toolchain@v1 24 | with: 25 | toolchain: ${{ matrix.rust }} 26 | target: ${{ matrix.target }} 27 | override: true 28 | - name: Run cargo check 29 | uses: actions-rs/cargo@v1 30 | with: 31 | command: check 32 | - name: Run tests 33 | run: TERM=linux cargo test --verbose 34 | 35 | rustfmt: 36 | name: rustfmt 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout repository 40 | uses: actions/checkout@v2 41 | - name: Install Rust 42 | uses: actions-rs/toolchain@v1 43 | with: 44 | toolchain: stable 45 | override: true 46 | profile: minimal 47 | components: rustfmt 48 | - name: Check formatting 49 | run: | 50 | cargo fmt --all -- --check 51 | -------------------------------------------------------------------------------- /src/draw.rs: -------------------------------------------------------------------------------- 1 | ///! A trait defines something that could be drawn 2 | use crate::canvas::Canvas; 3 | 4 | pub type DrawResult = std::result::Result>; 5 | 6 | /// Something that knows how to draw itself onto the canvas 7 | #[allow(unused_variables)] 8 | pub trait Draw { 9 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 10 | Ok(()) 11 | } 12 | fn draw_mut(&mut self, canvas: &mut dyn Canvas) -> DrawResult<()> { 13 | self.draw(canvas) 14 | } 15 | } 16 | 17 | impl Draw for &T { 18 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 19 | (*self).draw(canvas) 20 | } 21 | fn draw_mut(&mut self, canvas: &mut dyn Canvas) -> DrawResult<()> { 22 | (*self).draw(canvas) 23 | } 24 | } 25 | 26 | impl Draw for &mut T { 27 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 28 | (**self).draw(canvas) 29 | } 30 | fn draw_mut(&mut self, canvas: &mut dyn Canvas) -> DrawResult<()> { 31 | (**self).draw_mut(canvas) 32 | } 33 | } 34 | 35 | impl Draw for Box { 36 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 37 | self.as_ref().draw(canvas) 38 | } 39 | 40 | fn draw_mut(&mut self, canvas: &mut dyn Canvas) -> DrawResult<()> { 41 | self.as_mut().draw_mut(canvas) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/color.rs: -------------------------------------------------------------------------------- 1 | /// Color of a character, could be 8 bit(256 color) or RGB color 2 | /// 3 | /// ``` 4 | /// use tuikit::attr::Color; 5 | /// Color::RED; // predefined values 6 | /// Color::Rgb(255, 0, 0); // RED 7 | /// ``` 8 | #[derive(Debug, Clone, Copy, PartialEq)] 9 | pub enum Color { 10 | Default, 11 | AnsiValue(u8), 12 | Rgb(u8, u8, u8), 13 | 14 | #[doc(hidden)] 15 | __Nonexhaustive, 16 | } 17 | 18 | impl Color { 19 | pub const BLACK: Color = Color::AnsiValue(0); 20 | pub const RED: Color = Color::AnsiValue(1); 21 | pub const GREEN: Color = Color::AnsiValue(2); 22 | pub const YELLOW: Color = Color::AnsiValue(3); 23 | pub const BLUE: Color = Color::AnsiValue(4); 24 | pub const MAGENTA: Color = Color::AnsiValue(5); 25 | pub const CYAN: Color = Color::AnsiValue(6); 26 | pub const WHITE: Color = Color::AnsiValue(7); 27 | pub const LIGHT_BLACK: Color = Color::AnsiValue(8); 28 | pub const LIGHT_RED: Color = Color::AnsiValue(9); 29 | pub const LIGHT_GREEN: Color = Color::AnsiValue(10); 30 | pub const LIGHT_YELLOW: Color = Color::AnsiValue(11); 31 | pub const LIGHT_BLUE: Color = Color::AnsiValue(12); 32 | pub const LIGHT_MAGENTA: Color = Color::AnsiValue(13); 33 | pub const LIGHT_CYAN: Color = Color::AnsiValue(14); 34 | pub const LIGHT_WHITE: Color = Color::AnsiValue(15); 35 | } 36 | 37 | impl Default for Color { 38 | fn default() -> Self { 39 | Color::Default 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/256color_on_screen.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use tuikit::attr::{Attr, Color}; 3 | use tuikit::canvas::Canvas; 4 | use tuikit::output::Output; 5 | use tuikit::screen::Screen; 6 | 7 | fn main() { 8 | let mut output = Output::new(Box::new(io::stdout())).unwrap(); 9 | let (width, height) = output.terminal_size().unwrap(); 10 | let mut screen = Screen::new(width, height); 11 | 12 | for fg in 0..=255 { 13 | let _ = screen.print_with_attr( 14 | fg / 16, 15 | (fg % 16) * 5, 16 | format!("{:5}", fg).as_str(), 17 | Color::AnsiValue(fg as u8).into(), 18 | ); 19 | } 20 | 21 | let _ = screen.set_cursor(15, 80); 22 | let commands = screen.present(); 23 | 24 | commands.into_iter().for_each(|cmd| output.execute(cmd)); 25 | output.flush(); 26 | 27 | let _ = screen.print_with_attr(0, 78, "HELLO WORLD", Attr::default()); 28 | let commands = screen.present(); 29 | 30 | commands.into_iter().for_each(|cmd| output.execute(cmd)); 31 | output.flush(); 32 | 33 | for bg in 0..=255 { 34 | let _ = screen.print_with_attr( 35 | bg / 16, 36 | (bg % 16) * 5, 37 | format!("{:5}", bg).as_str(), 38 | Attr { 39 | bg: Color::AnsiValue(bg as u8), 40 | ..Attr::default() 41 | }, 42 | ); 43 | } 44 | let commands = screen.present(); 45 | commands.into_iter().for_each(|cmd| output.execute(cmd)); 46 | output.reset_attributes(); 47 | output.flush() 48 | } 49 | -------------------------------------------------------------------------------- /src/cell.rs: -------------------------------------------------------------------------------- 1 | ///! `Cell` is a cell of the terminal. 2 | ///! It has a display character and an attribute (fg and bg color, effects). 3 | use crate::attr::{Attr, Color, Effect}; 4 | 5 | const EMPTY_CHAR: char = '\0'; 6 | 7 | #[derive(Debug, Clone, Copy, PartialEq)] 8 | pub struct Cell { 9 | pub ch: char, 10 | pub attr: Attr, 11 | } 12 | 13 | impl Default for Cell { 14 | fn default() -> Self { 15 | Self { 16 | ch: ' ', 17 | attr: Attr::default(), 18 | } 19 | } 20 | } 21 | 22 | impl Cell { 23 | pub fn empty() -> Self { 24 | Self::default().ch(EMPTY_CHAR) 25 | } 26 | 27 | pub fn ch(mut self, ch: char) -> Self { 28 | self.ch = ch; 29 | self 30 | } 31 | 32 | pub fn fg(mut self, fg: Color) -> Self { 33 | self.attr.fg = fg; 34 | self 35 | } 36 | 37 | pub fn bg(mut self, bg: Color) -> Self { 38 | self.attr.bg = bg; 39 | self 40 | } 41 | 42 | pub fn effect(mut self, effect: Effect) -> Self { 43 | self.attr.effect = effect; 44 | self 45 | } 46 | 47 | pub fn attribute(mut self, attr: Attr) -> Self { 48 | self.attr = attr; 49 | self 50 | } 51 | 52 | /// check if a cell is empty 53 | pub fn is_empty(self) -> bool { 54 | self.ch == EMPTY_CHAR && self.attr == Attr::default() 55 | } 56 | } 57 | 58 | impl From for Cell { 59 | fn from(ch: char) -> Self { 60 | Cell { 61 | ch, 62 | attr: Attr::default(), 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/split.rs: -------------------------------------------------------------------------------- 1 | use tuikit::prelude::*; 2 | 3 | struct Fit(String); 4 | 5 | impl Draw for Fit { 6 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 7 | let (_width, height) = canvas.size()?; 8 | let top = height / 2; 9 | let _ = canvas.print(top, 0, &self.0); 10 | Ok(()) 11 | } 12 | } 13 | impl Widget for Fit { 14 | fn size_hint(&self) -> (Option, Option) { 15 | (Some(self.0.len()), None) 16 | } 17 | } 18 | 19 | struct Model(String); 20 | 21 | impl Draw for Model { 22 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 23 | let (width, height) = canvas.size()?; 24 | let message_width = self.0.len(); 25 | let left = (width - message_width) / 2; 26 | let top = height / 2; 27 | let _ = canvas.print_with_attr(0, left, "press 'q' to exit", Effect::UNDERLINE.into()); 28 | let _ = canvas.print(top, left, &self.0); 29 | Ok(()) 30 | } 31 | } 32 | 33 | impl Widget for Model {} 34 | 35 | fn main() { 36 | let term: Term<()> = Term::with_height(TermHeight::Percent(50)).unwrap(); 37 | let model = Model("Hey, I'm in middle!".to_string()); 38 | let fit = Fit("Short Text That Fits".to_string()); 39 | 40 | while let Ok(ev) = term.poll_event() { 41 | match ev { 42 | Event::Key(Key::Char('q')) | Event::Key(Key::Ctrl('c')) => break, 43 | _ => (), 44 | } 45 | 46 | let hsplit = HSplit::default() 47 | .split( 48 | VSplit::default() 49 | .shrink(0) 50 | .grow(0) 51 | .split(Win::new(&fit).border(true)) 52 | .split(Win::new(&fit).border(true)), 53 | ) 54 | .split(Win::new(&model).border(true)); 55 | 56 | let _ = term.draw(&hsplit); 57 | let _ = term.present(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/win.rs: -------------------------------------------------------------------------------- 1 | use tuikit::prelude::*; 2 | 3 | struct Model(String); 4 | 5 | impl Draw for Model { 6 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 7 | let (width, height) = canvas.size()?; 8 | let message_width = self.0.len(); 9 | let left = (width - message_width) / 2; 10 | let top = height / 2; 11 | let _ = canvas.print(top, left, &self.0); 12 | Ok(()) 13 | } 14 | } 15 | 16 | impl Widget for Model {} 17 | 18 | fn main() { 19 | let term: Term<()> = Term::with_options( 20 | TermOptions::default() 21 | .height(TermHeight::Percent(50)) 22 | .disable_alternate_screen(true) 23 | .clear_on_start(false), 24 | ) 25 | .unwrap(); 26 | let model = Model("Hey, I'm in middle!".to_string()); 27 | 28 | while let Ok(ev) = term.poll_event() { 29 | match ev { 30 | Event::Key(Key::Char('q')) | Event::Key(Key::Ctrl('c')) => break, 31 | _ => (), 32 | } 33 | let _ = term.print(0, 0, "press 'q' to exit"); 34 | 35 | let inner_win = Win::new(&model) 36 | .fn_draw_header(Box::new(|canvas| { 37 | let _ = canvas.print(0, 0, "header printed with function"); 38 | Ok(()) 39 | })) 40 | .border(true); 41 | 42 | let win_bottom_title = Win::new(&inner_win) 43 | .title_align(HorizontalAlign::Center) 44 | .title("Title (at bottom) center aligned") 45 | .right_prompt("Right Prompt stays") 46 | .title_on_top(false) 47 | .border_bottom(true); 48 | 49 | let win = Win::new(&win_bottom_title) 50 | .margin(Size::Percent(10)) 51 | .padding(1) 52 | .title("Window Title") 53 | .right_prompt("Right Prompt") 54 | .border(true) 55 | .border_top_attr(Color::BLUE) 56 | .border_right_attr(Color::YELLOW) 57 | .border_bottom_attr(Color::RED) 58 | .border_left_attr(Color::GREEN); 59 | 60 | let _ = term.draw(&win); 61 | let _ = term.present(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/stack.rs: -------------------------------------------------------------------------------- 1 | use tuikit::prelude::*; 2 | 3 | struct Model(String); 4 | 5 | impl Draw for Model { 6 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 7 | let (width, height) = canvas.size()?; 8 | let message_width = self.0.len(); 9 | let left = (width - message_width) / 2; 10 | let top = height / 2; 11 | let _ = canvas.print(top, left, &self.0); 12 | Ok(()) 13 | } 14 | } 15 | 16 | impl Widget for Model { 17 | fn on_event(&self, event: Event, _rect: Rectangle) -> Vec { 18 | if let Event::Key(Key::MousePress(_, _, _)) = event { 19 | vec![format!("{} clicked", self.0)] 20 | } else { 21 | vec![] 22 | } 23 | } 24 | } 25 | 26 | fn main() { 27 | let term = Term::with_options(TermOptions::default().mouse_enabled(true)).unwrap(); 28 | let (width, height) = term.term_size().unwrap(); 29 | 30 | while let Ok(ev) = term.poll_event() { 31 | match ev { 32 | Event::Key(Key::Char('q')) | Event::Key(Key::Ctrl('c')) => break, 33 | _ => (), 34 | } 35 | let _ = term.print(1, 1, "press 'q' to exit, try click on windows"); 36 | 37 | let stack = Stack::::new() 38 | .top( 39 | Win::new(Model("win floating on top".to_string())) 40 | .border(true) 41 | .margin(Size::Percent(30)), 42 | ) 43 | .bottom( 44 | HSplit::default() 45 | .split(Win::new(Model("left".to_string())).border(true)) 46 | .split(Win::new(Model("right".to_string())).border(true)), 47 | ); 48 | 49 | let message = stack.on_event( 50 | ev, 51 | Rectangle { 52 | width, 53 | height, 54 | top: 0, 55 | left: 0, 56 | }, 57 | ); 58 | let click_message = if message.is_empty() { "" } else { &message[0] }; 59 | let _ = term.print(2, 1, click_message); 60 | let _ = term.draw(&stack); 61 | let _ = term.present(); 62 | } 63 | let _ = term.set_cursor(0, 0); 64 | let _ = term.show_cursor(true); 65 | } 66 | -------------------------------------------------------------------------------- /examples/true_color.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use tuikit::attr::Color; 3 | use tuikit::output::Output; 4 | 5 | // ported from: https://github.com/gnachman/iTerm2/blob/master/tests/24-bit-color.sh 6 | // should be run in terminals that supports true color 7 | 8 | // given a color idx/22 along HSV, return (r, g, b) 9 | fn rainbow_color(idx: u8) -> (u8, u8, u8) { 10 | let h = idx / 43; 11 | let f = idx - 43 * h; 12 | let t = ((f as i32 * 255) / 43) as u8; 13 | let q = 255 - t; 14 | 15 | match h { 16 | 0 => (255, t, 0), 17 | 1 => (q, 255, 0), 18 | 2 => (0, 255, t), 19 | 3 => (0, q, 255), 20 | 4 => (t, 0, 255), 21 | 5 => (255, 0, q), 22 | _ => unreachable!(), 23 | } 24 | } 25 | 26 | fn try_background(output: &mut Output, r: u8, g: u8, b: u8) { 27 | output.set_bg(Color::Rgb(r, g, b)); 28 | output.write(" ") 29 | } 30 | 31 | fn reset_output(output: &mut Output) { 32 | output.reset_attributes(); 33 | output.write("\n"); 34 | output.flush(); 35 | } 36 | 37 | fn main() { 38 | let mut output = Output::new(Box::new(io::stdout())).unwrap(); 39 | for i in 0..=127 { 40 | try_background(&mut output, i, 0, 0); 41 | } 42 | reset_output(&mut output); 43 | 44 | for i in (128..=255).rev() { 45 | try_background(&mut output, i, 0, 0); 46 | } 47 | reset_output(&mut output); 48 | 49 | for i in 0..=127 { 50 | try_background(&mut output, 0, i, 0); 51 | } 52 | reset_output(&mut output); 53 | 54 | for i in (128..=255).rev() { 55 | try_background(&mut output, 0, i, 0); 56 | } 57 | reset_output(&mut output); 58 | 59 | for i in 0..=127 { 60 | try_background(&mut output, 0, 0, i); 61 | } 62 | reset_output(&mut output); 63 | 64 | for i in (128..=255).rev() { 65 | try_background(&mut output, 0, 0, i); 66 | } 67 | reset_output(&mut output); 68 | 69 | for i in 0..=127 { 70 | let (r, g, b) = rainbow_color(i); 71 | try_background(&mut output, r, g, b); 72 | } 73 | reset_output(&mut output); 74 | 75 | for i in (128..=255).rev() { 76 | let (r, g, b) = rainbow_color(i); 77 | try_background(&mut output, r, g, b); 78 | } 79 | reset_output(&mut output); 80 | } 81 | -------------------------------------------------------------------------------- /examples/termbox.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::thread; 3 | use std::time::{Duration, Instant}; 4 | use tuikit::prelude::*; 5 | 6 | extern crate env_logger; 7 | 8 | /// This example is testing tuikit with multi-threads. 9 | 10 | const COL: usize = 4; 11 | 12 | fn main() { 13 | env_logger::init(); 14 | let term = Arc::new(Term::with_height(TermHeight::Fixed(10)).unwrap()); 15 | let _ = term.enable_mouse_support(); 16 | let now = Instant::now(); 17 | 18 | print_banner(&term); 19 | 20 | let th = thread::spawn(move || { 21 | while let Ok(ev) = term.poll_event() { 22 | match ev { 23 | Event::Key(Key::Char('q')) | Event::Key(Key::Ctrl('c')) => break, 24 | Event::Key(Key::Char('r')) => { 25 | let term = term.clone(); 26 | thread::spawn(move || { 27 | let _ = term.pause(); 28 | println!("restart in 2 seconds"); 29 | thread::sleep(Duration::from_secs(2)); 30 | let _ = term.restart(); 31 | let _ = term.clear(); 32 | }); 33 | } 34 | _ => (), 35 | } 36 | 37 | print_banner(&term); 38 | print_event(&term, ev, &now); 39 | } 40 | }); 41 | let _ = th.join(); 42 | } 43 | 44 | fn print_banner(term: &Term) { 45 | let (_, height) = term.term_size().unwrap_or((5, 5)); 46 | for row in 0..height { 47 | let _ = term.print(row, 0, format!("{} ", row).as_str()); 48 | } 49 | let attr = Attr { 50 | fg: Color::GREEN, 51 | effect: Effect::UNDERLINE, 52 | ..Attr::default() 53 | }; 54 | let _ = term.print_with_attr(0, COL, "How to use: (q)uit, (r)estart", attr); 55 | let _ = term.present(); 56 | } 57 | 58 | fn print_event(term: &Term, ev: Event, now: &Instant) { 59 | let elapsed = now.elapsed(); 60 | let (_, height) = term.term_size().unwrap_or((5, 5)); 61 | let _ = term.print(1, COL, format!("{:?}", ev).as_str()); 62 | let _ = term.print( 63 | height - 1, 64 | COL, 65 | format!( 66 | "time elapsed since program start: {}s + {}ms", 67 | elapsed.as_secs(), 68 | elapsed.subsec_millis() 69 | ) 70 | .as_str(), 71 | ); 72 | let _ = term.present(); 73 | } 74 | -------------------------------------------------------------------------------- /src/sys/signal.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use nix::sys::signal::{pthread_sigmask, sigaction}; 3 | use nix::sys::signal::{SaFlags, SigAction, SigHandler, SigSet, SigmaskHow, Signal}; 4 | use std::collections::HashMap; 5 | use std::sync::atomic::{AtomicUsize, Ordering}; 6 | use std::sync::mpsc::{channel, Receiver, Sender}; 7 | use std::sync::Mutex; 8 | use std::sync::Once; 9 | use std::thread; 10 | 11 | lazy_static! { 12 | static ref NOTIFIER_COUNTER: AtomicUsize = AtomicUsize::new(1); 13 | static ref NOTIFIER: Mutex>> = Mutex::new(HashMap::new()); 14 | } 15 | 16 | static ONCE: Once = Once::new(); 17 | 18 | pub fn initialize_signals() { 19 | ONCE.call_once(listen_sigwinch); 20 | } 21 | 22 | pub fn notify_on_sigwinch() -> (usize, Receiver<()>) { 23 | let (tx, rx) = channel(); 24 | let new_id = NOTIFIER_COUNTER.fetch_add(1, Ordering::Relaxed); 25 | let mut notifiers = NOTIFIER.lock().unwrap(); 26 | notifiers.entry(new_id).or_insert(tx); 27 | (new_id, rx) 28 | } 29 | 30 | pub fn unregister_sigwinch(id: usize) -> Option> { 31 | let mut notifiers = NOTIFIER.lock().unwrap(); 32 | notifiers.remove(&id) 33 | } 34 | 35 | extern "C" fn handle_sigwiwnch(_: i32) {} 36 | 37 | fn listen_sigwinch() { 38 | let (tx_sig, rx_sig) = channel(); 39 | 40 | // register terminal resize event, `pthread_sigmask` should be run before any thread. 41 | let mut sigset = SigSet::empty(); 42 | sigset.add(Signal::SIGWINCH); 43 | let _ = pthread_sigmask(SigmaskHow::SIG_BLOCK, Some(&sigset), None); 44 | 45 | // SIGWINCH is ignored by mac by default, thus we need to register an empty handler 46 | let action = SigAction::new( 47 | SigHandler::Handler(handle_sigwiwnch), 48 | SaFlags::empty(), 49 | SigSet::empty(), 50 | ); 51 | 52 | unsafe { 53 | let _ = sigaction(Signal::SIGWINCH, &action); 54 | } 55 | 56 | thread::spawn(move || { 57 | // listen to the resize event; 58 | loop { 59 | let _errno = sigset.wait(); 60 | let _ = tx_sig.send(()); 61 | } 62 | }); 63 | 64 | thread::spawn(move || { 65 | while let Ok(_) = rx_sig.recv() { 66 | let notifiers = NOTIFIER.lock().unwrap(); 67 | for (_, sender) in notifiers.iter() { 68 | let _ = sender.send(()); 69 | } 70 | } 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! ## Tuikit 3 | //! Tuikit is a TUI library for writing terminal UI applications. Highlights: 4 | //! 5 | //! - Thread safe. 6 | //! - Support non-fullscreen mode as well as fullscreen mode. 7 | //! - Support `Alt` keys, mouse events, etc. 8 | //! - Buffering for efficient rendering. 9 | //! 10 | //! Tuikit is modeld after [termbox](https://github.com/nsf/termbox) which views the 11 | //! terminal as a table of fixed-size cells and input being a stream of structured 12 | //! messages. 13 | //! 14 | //! ## Usage 15 | //! 16 | //! In your `Cargo.toml` add the following: 17 | //! 18 | //! ```toml 19 | //! [dependencies] 20 | //! tuikit = "*" 21 | //! ``` 22 | //! 23 | //! Here is an example: 24 | //! 25 | //! ```no_run 26 | //! use tuikit::attr::*; 27 | //! use tuikit::term::{Term, TermHeight}; 28 | //! use tuikit::event::{Event, Key}; 29 | //! use std::cmp::{min, max}; 30 | //! 31 | //! fn main() { 32 | //! let term: Term<()> = Term::with_height(TermHeight::Percent(30)).unwrap(); 33 | //! let mut row = 1; 34 | //! let mut col = 0; 35 | //! 36 | //! let _ = term.print(0, 0, "press arrow key to move the text, (q) to quit"); 37 | //! let _ = term.present(); 38 | //! 39 | //! while let Ok(ev) = term.poll_event() { 40 | //! let _ = term.clear(); 41 | //! let _ = term.print(0, 0, "press arrow key to move the text, (q) to quit"); 42 | //! 43 | //! let (width, height) = term.term_size().unwrap(); 44 | //! match ev { 45 | //! Event::Key(Key::ESC) | Event::Key(Key::Char('q')) => break, 46 | //! Event::Key(Key::Up) => row = max(row-1, 1), 47 | //! Event::Key(Key::Down) => row = min(row+1, height-1), 48 | //! Event::Key(Key::Left) => col = max(col, 1)-1, 49 | //! Event::Key(Key::Right) => col = min(col+1, width-1), 50 | //! _ => {} 51 | //! } 52 | //! 53 | //! let attr = Attr{ fg: Color::RED, ..Attr::default() }; 54 | //! let _ = term.print_with_attr(row, col, "Hello World! 你好!今日は。", attr); 55 | //! let _ = term.set_cursor(row, col); 56 | //! let _ = term.present(); 57 | //! } 58 | //! } 59 | //! ``` 60 | pub mod attr; 61 | pub mod canvas; 62 | pub mod cell; 63 | mod color; 64 | pub mod draw; 65 | pub mod error; 66 | pub mod event; 67 | pub mod input; 68 | pub mod key; 69 | mod macros; 70 | pub mod output; 71 | pub mod prelude; 72 | pub mod raw; 73 | pub mod screen; 74 | mod spinlock; 75 | mod sys; 76 | pub mod term; 77 | pub mod widget; 78 | 79 | #[macro_use] 80 | extern crate log; 81 | 82 | use crate::error::TuikitError; 83 | 84 | pub type Result = std::result::Result; 85 | -------------------------------------------------------------------------------- /src/widget/util.rs: -------------------------------------------------------------------------------- 1 | use crate::event::Event; 2 | use crate::key::Key; 3 | use crate::widget::Rectangle; 4 | 5 | pub fn adjust_event(event: Event, inner_rect: Rectangle) -> Option { 6 | match event { 7 | Event::Key(Key::MousePress(button, row, col)) => { 8 | if inner_rect.contains(row as usize, col as usize) { 9 | let (row, col) = inner_rect.relative_to_origin(row as usize, col as usize); 10 | Some(Event::Key(Key::MousePress(button, row as u16, col as u16))) 11 | } else { 12 | None 13 | } 14 | } 15 | Event::Key(Key::MouseRelease(row, col)) => { 16 | if inner_rect.contains(row as usize, col as usize) { 17 | let (row, col) = inner_rect.relative_to_origin(row as usize, col as usize); 18 | Some(Event::Key(Key::MouseRelease(row as u16, col as u16))) 19 | } else { 20 | None 21 | } 22 | } 23 | Event::Key(Key::MouseHold(row, col)) => { 24 | if inner_rect.contains(row as usize, col as usize) { 25 | let (row, col) = inner_rect.relative_to_origin(row as usize, col as usize); 26 | Some(Event::Key(Key::MouseHold(row as u16, col as u16))) 27 | } else { 28 | None 29 | } 30 | } 31 | Event::Key(Key::SingleClick(button, row, col)) => { 32 | if inner_rect.contains(row as usize, col as usize) { 33 | let (row, col) = inner_rect.relative_to_origin(row as usize, col as usize); 34 | Some(Event::Key(Key::SingleClick(button, row as u16, col as u16))) 35 | } else { 36 | None 37 | } 38 | } 39 | Event::Key(Key::DoubleClick(button, row, col)) => { 40 | if inner_rect.contains(row as usize, col as usize) { 41 | let (row, col) = inner_rect.relative_to_origin(row as usize, col as usize); 42 | Some(Event::Key(Key::DoubleClick(button, row as u16, col as u16))) 43 | } else { 44 | None 45 | } 46 | } 47 | Event::Key(Key::WheelDown(row, col, count)) => { 48 | if inner_rect.contains(row as usize, col as usize) { 49 | let (row, col) = inner_rect.relative_to_origin(row as usize, col as usize); 50 | Some(Event::Key(Key::WheelDown(row as u16, col as u16, count))) 51 | } else { 52 | None 53 | } 54 | } 55 | Event::Key(Key::WheelUp(row, col, count)) => { 56 | if inner_rect.contains(row as usize, col as usize) { 57 | let (row, col) = inner_rect.relative_to_origin(row as usize, col as usize); 58 | Some(Event::Key(Key::WheelUp(row as u16, col as u16, count))) 59 | } else { 60 | None 61 | } 62 | } 63 | ev => Some(ev), 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/attr.rs: -------------------------------------------------------------------------------- 1 | //! attr modules defines the attributes(colors, effects) of a terminal cell 2 | 3 | use bitflags::bitflags; 4 | 5 | pub use crate::color::Color; 6 | 7 | /// `Attr` is a rendering attribute that contains fg color, bg color and text effect. 8 | /// 9 | /// ``` 10 | /// use tuikit::attr::{Attr, Effect, Color}; 11 | /// 12 | /// let attr = Attr { fg: Color::RED, effect: Effect::BOLD, ..Attr::default() }; 13 | /// ``` 14 | #[derive(Debug, Clone, Copy, PartialEq)] 15 | pub struct Attr { 16 | pub fg: Color, 17 | pub bg: Color, 18 | pub effect: Effect, 19 | } 20 | 21 | impl Default for Attr { 22 | fn default() -> Self { 23 | Attr { 24 | fg: Color::default(), 25 | bg: Color::default(), 26 | effect: Effect::empty(), 27 | } 28 | } 29 | } 30 | 31 | impl Attr { 32 | /// extend the properties with the new attr's if the properties in new attr is not default. 33 | /// ``` 34 | /// use tuikit::attr::{Attr, Color, Effect}; 35 | /// 36 | /// let default = Attr{fg: Color::BLUE, bg: Color::YELLOW, effect: Effect::BOLD}; 37 | /// let new = Attr{fg: Color::Default, bg: Color::WHITE, effect: Effect::REVERSE}; 38 | /// let extended = default.extend(new); 39 | /// 40 | /// assert_eq!(Color::BLUE, extended.fg); 41 | /// assert_eq!(Color::WHITE, extended.bg); 42 | /// assert_eq!(Effect::BOLD | Effect::REVERSE, extended.effect); 43 | /// ``` 44 | pub fn extend(&self, new_attr: Self) -> Attr { 45 | Attr { 46 | fg: if new_attr.fg != Color::default() { 47 | new_attr.fg 48 | } else { 49 | self.fg 50 | }, 51 | bg: if new_attr.bg != Color::default() { 52 | new_attr.bg 53 | } else { 54 | self.bg 55 | }, 56 | effect: self.effect | new_attr.effect, 57 | } 58 | } 59 | 60 | pub fn fg(mut self, fg: Color) -> Self { 61 | self.fg = fg; 62 | self 63 | } 64 | 65 | pub fn bg(mut self, bg: Color) -> Self { 66 | self.bg = bg; 67 | self 68 | } 69 | 70 | pub fn effect(mut self, effect: Effect) -> Self { 71 | self.effect = effect; 72 | self 73 | } 74 | } 75 | 76 | bitflags! { 77 | /// `Effect` is the effect of a text 78 | pub struct Effect: u8 { 79 | const BOLD = 0b00000001; 80 | const DIM = 0b00000010; 81 | const UNDERLINE = 0b00000100; 82 | const BLINK = 0b00001000; 83 | const REVERSE = 0b00010000; 84 | } 85 | } 86 | 87 | impl From for Attr { 88 | fn from(fg: Color) -> Self { 89 | Attr { 90 | fg, 91 | ..Default::default() 92 | } 93 | } 94 | } 95 | 96 | impl From for Attr { 97 | fn from(effect: Effect) -> Self { 98 | Attr { 99 | effect, 100 | ..Default::default() 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt::{Display, Formatter}; 3 | use std::string::FromUtf8Error; 4 | use std::time::Duration; 5 | 6 | #[derive(Debug)] 7 | pub enum TuikitError { 8 | UnknownSequence(String), 9 | NoCursorReportResponse, 10 | IndexOutOfBound(usize, usize), 11 | Timeout(Duration), 12 | Interrupted, 13 | TerminalNotStarted, 14 | DrawError(Box), 15 | SendEventError(String), 16 | FromUtf8Error(std::string::FromUtf8Error), 17 | ParseIntError(std::num::ParseIntError), 18 | IOError(std::io::Error), 19 | NixError(nix::Error), 20 | ChannelReceiveError(std::sync::mpsc::RecvError), 21 | } 22 | 23 | impl Display for TuikitError { 24 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 25 | match self { 26 | TuikitError::UnknownSequence(sequence) => { 27 | write!(f, "unsupported esc sequence: {}", sequence) 28 | } 29 | TuikitError::NoCursorReportResponse => { 30 | write!(f, "buffer did not contain cursor position response") 31 | } 32 | TuikitError::IndexOutOfBound(row, col) => { 33 | write!(f, "({}, {}) is out of bound", row, col) 34 | } 35 | TuikitError::Timeout(duration) => write!(f, "timeout with duration: {:?}", duration), 36 | TuikitError::Interrupted => write!(f, "interrupted"), 37 | TuikitError::TerminalNotStarted => { 38 | write!(f, "terminal not started, call `restart` to start it") 39 | } 40 | TuikitError::DrawError(error) => write!(f, "draw error: {}", error), 41 | TuikitError::SendEventError(error) => write!(f, "send event error: {}", error), 42 | TuikitError::FromUtf8Error(error) => write!(f, "{}", error), 43 | TuikitError::ParseIntError(error) => write!(f, "{}", error), 44 | TuikitError::IOError(error) => write!(f, "{}", error), 45 | TuikitError::NixError(error) => write!(f, "{}", error), 46 | TuikitError::ChannelReceiveError(error) => write!(f, "{}", error), 47 | } 48 | } 49 | } 50 | 51 | impl Error for TuikitError {} 52 | 53 | impl From for TuikitError { 54 | fn from(error: FromUtf8Error) -> Self { 55 | TuikitError::FromUtf8Error(error) 56 | } 57 | } 58 | 59 | impl From for TuikitError { 60 | fn from(error: std::num::ParseIntError) -> Self { 61 | TuikitError::ParseIntError(error) 62 | } 63 | } 64 | 65 | impl From for TuikitError { 66 | fn from(error: nix::Error) -> Self { 67 | TuikitError::NixError(error) 68 | } 69 | } 70 | 71 | impl From for TuikitError { 72 | fn from(error: std::io::Error) -> Self { 73 | TuikitError::IOError(error) 74 | } 75 | } 76 | 77 | impl From for TuikitError { 78 | fn from(error: std::sync::mpsc::RecvError) -> Self { 79 | TuikitError::ChannelReceiveError(error) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/spinlock.rs: -------------------------------------------------------------------------------- 1 | ///! SpinLock implemented using AtomicBool 2 | use std::cell::UnsafeCell; 3 | use std::ops::Deref; 4 | use std::ops::DerefMut; 5 | use std::sync::atomic::AtomicBool; 6 | use std::sync::atomic::Ordering; 7 | 8 | /// SpinLock implemented using AtomicBool 9 | /// Just like Mutex except: 10 | /// 11 | /// 1. It uses CAS for locking, more efficient in low contention 12 | /// 2. Use `.lock()` instead of `.lock().unwrap()` to retrieve the guard. 13 | /// 3. It doesn't handle poison so data is still available on thread panic. 14 | pub struct SpinLock { 15 | locked: AtomicBool, 16 | data: UnsafeCell, 17 | } 18 | 19 | unsafe impl Send for SpinLock {} 20 | unsafe impl Sync for SpinLock {} 21 | 22 | pub struct SpinLockGuard<'a, T: ?Sized + 'a> { 23 | // funny underscores due to how Deref/DerefMut currently work (they 24 | // disregard field privacy). 25 | __lock: &'a SpinLock, 26 | } 27 | 28 | impl<'a, T: ?Sized + 'a> SpinLockGuard<'a, T> { 29 | pub fn new(pool: &'a SpinLock) -> SpinLockGuard<'a, T> { 30 | Self { __lock: pool } 31 | } 32 | } 33 | 34 | unsafe impl<'a, T: ?Sized + Sync> Sync for SpinLockGuard<'a, T> {} 35 | 36 | impl SpinLock { 37 | pub fn new(t: T) -> SpinLock { 38 | Self { 39 | locked: AtomicBool::new(false), 40 | data: UnsafeCell::new(t), 41 | } 42 | } 43 | } 44 | 45 | impl SpinLock { 46 | pub fn lock(&self) -> SpinLockGuard { 47 | while let Err(_) = 48 | self.locked 49 | .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) 50 | {} 51 | SpinLockGuard::new(self) 52 | } 53 | } 54 | 55 | impl<'mutex, T: ?Sized> Deref for SpinLockGuard<'mutex, T> { 56 | type Target = T; 57 | 58 | fn deref(&self) -> &T { 59 | unsafe { &*self.__lock.data.get() } 60 | } 61 | } 62 | 63 | impl<'mutex, T: ?Sized> DerefMut for SpinLockGuard<'mutex, T> { 64 | fn deref_mut(&mut self) -> &mut T { 65 | unsafe { &mut *self.__lock.data.get() } 66 | } 67 | } 68 | 69 | impl<'a, T: ?Sized> Drop for SpinLockGuard<'a, T> { 70 | #[inline] 71 | fn drop(&mut self) { 72 | while let Err(_) = 73 | self.__lock 74 | .locked 75 | .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) 76 | {} 77 | } 78 | } 79 | 80 | #[cfg(test)] 81 | mod tests { 82 | use super::*; 83 | use std::sync::mpsc::channel; 84 | use std::sync::Arc; 85 | use std::thread; 86 | 87 | #[derive(Eq, PartialEq, Debug)] 88 | struct NonCopy(i32); 89 | 90 | #[test] 91 | fn smoke() { 92 | let m = SpinLock::new(()); 93 | drop(m.lock()); 94 | drop(m.lock()); 95 | } 96 | 97 | #[test] 98 | fn lots_and_lots() { 99 | const J: u32 = 1000; 100 | const K: u32 = 3; 101 | 102 | let m = Arc::new(SpinLock::new(0)); 103 | 104 | fn inc(m: &SpinLock) { 105 | for _ in 0..J { 106 | *m.lock() += 1; 107 | } 108 | } 109 | 110 | let (tx, rx) = channel(); 111 | for _ in 0..K { 112 | let tx2 = tx.clone(); 113 | let m2 = m.clone(); 114 | thread::spawn(move || { 115 | inc(&m2); 116 | tx2.send(()).unwrap(); 117 | }); 118 | let tx2 = tx.clone(); 119 | let m2 = m.clone(); 120 | thread::spawn(move || { 121 | inc(&m2); 122 | tx2.send(()).unwrap(); 123 | }); 124 | } 125 | 126 | drop(tx); 127 | for _ in 0..2 * K { 128 | rx.recv().unwrap(); 129 | } 130 | assert_eq!(*m.lock(), J * K * 2); 131 | } 132 | 133 | #[test] 134 | fn test_mutex_unsized() { 135 | let mutex: &SpinLock<[i32]> = &SpinLock::new([1, 2, 3]); 136 | { 137 | let b = &mut *mutex.lock(); 138 | b[0] = 4; 139 | b[2] = 5; 140 | } 141 | let comp: &[i32] = &[4, 2, 5]; 142 | assert_eq!(&*mutex.lock(), comp); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/canvas.rs: -------------------------------------------------------------------------------- 1 | ///! A canvas is a trait defining the draw actions 2 | use crate::attr::Attr; 3 | use crate::cell::Cell; 4 | use crate::Result; 5 | use unicode_width::UnicodeWidthChar; 6 | 7 | pub trait Canvas { 8 | /// Get the canvas size (width, height) 9 | fn size(&self) -> Result<(usize, usize)>; 10 | 11 | /// clear the canvas 12 | fn clear(&mut self) -> Result<()>; 13 | 14 | /// change a cell of position `(row, col)` to `cell` 15 | /// if `(row, col)` is out of boundary, `Ok` is returned, but no operation is taken 16 | /// return the width of the character/cell 17 | fn put_cell(&mut self, row: usize, col: usize, cell: Cell) -> Result; 18 | 19 | /// just like put_cell, except it accept (char & attr) 20 | /// return the width of the character/cell 21 | fn put_char_with_attr( 22 | &mut self, 23 | row: usize, 24 | col: usize, 25 | ch: char, 26 | attr: Attr, 27 | ) -> Result { 28 | self.put_cell(row, col, Cell { ch, attr }) 29 | } 30 | 31 | /// print `content` starting with position `(row, col)` with `attr` 32 | /// - canvas should NOT wrap to y+1 if the content is too long 33 | /// - canvas should handle wide characters 34 | /// return the printed width of the content 35 | fn print_with_attr( 36 | &mut self, 37 | row: usize, 38 | col: usize, 39 | content: &str, 40 | attr: Attr, 41 | ) -> Result { 42 | let mut cell = Cell { 43 | attr, 44 | ..Cell::default() 45 | }; 46 | 47 | let mut width = 0; 48 | for ch in content.chars() { 49 | cell.ch = ch; 50 | width += self.put_cell(row, col + width, cell)?; 51 | } 52 | Ok(width) 53 | } 54 | 55 | /// print `content` starting with position `(row, col)` with default attribute 56 | fn print(&mut self, row: usize, col: usize, content: &str) -> Result { 57 | self.print_with_attr(row, col, content, Attr::default()) 58 | } 59 | 60 | /// move cursor position (row, col) and show cursor 61 | fn set_cursor(&mut self, row: usize, col: usize) -> Result<()>; 62 | 63 | /// show/hide cursor, set `show` to `false` to hide the cursor 64 | fn show_cursor(&mut self, show: bool) -> Result<()>; 65 | } 66 | 67 | /// A sub-area of a canvas. 68 | /// It will handle the adjustments of cursor movement, so that you could write 69 | /// to for example (0, 0) and BoundedCanvas will adjust it to real position. 70 | pub struct BoundedCanvas<'a> { 71 | canvas: &'a mut dyn Canvas, 72 | top: usize, 73 | left: usize, 74 | width: usize, 75 | height: usize, 76 | } 77 | 78 | impl<'a> BoundedCanvas<'a> { 79 | pub fn new( 80 | top: usize, 81 | left: usize, 82 | width: usize, 83 | height: usize, 84 | canvas: &'a mut dyn Canvas, 85 | ) -> Self { 86 | Self { 87 | canvas, 88 | top, 89 | left, 90 | width, 91 | height, 92 | } 93 | } 94 | } 95 | 96 | impl<'a> Canvas for BoundedCanvas<'a> { 97 | fn size(&self) -> Result<(usize, usize)> { 98 | Ok((self.width, self.height)) 99 | } 100 | 101 | fn clear(&mut self) -> Result<()> { 102 | for row in self.top..(self.top + self.height) { 103 | for col in self.left..(self.left + self.width) { 104 | let _ = self.canvas.put_cell(row, col, Cell::empty()); 105 | } 106 | } 107 | 108 | Ok(()) 109 | } 110 | 111 | fn put_cell(&mut self, row: usize, col: usize, cell: Cell) -> Result { 112 | if row >= self.height || col >= self.width { 113 | // do nothing 114 | Ok(cell.ch.width().unwrap_or(2)) 115 | } else { 116 | self.canvas.put_cell(row + self.top, col + self.left, cell) 117 | } 118 | } 119 | 120 | fn set_cursor(&mut self, row: usize, col: usize) -> Result<()> { 121 | if row >= self.height || col >= self.width { 122 | // do nothing 123 | Ok(()) 124 | } else { 125 | self.canvas.set_cursor(row + self.top, col + self.left) 126 | } 127 | } 128 | 129 | fn show_cursor(&mut self, show: bool) -> Result<()> { 130 | self.canvas.show_cursor(show) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/raw.rs: -------------------------------------------------------------------------------- 1 | //! Managing raw mode. 2 | //! 3 | //! Raw mode is a particular state a TTY can have. It signifies that: 4 | //! 5 | //! 1. No line buffering (the input is given byte-by-byte). 6 | //! 2. The input is not written out, instead it has to be done manually by the programmer. 7 | //! 3. The output is not canonicalized (for example, `\n` means "go one line down", not "line 8 | //! break"). 9 | //! 10 | //! # Example 11 | //! 12 | //! ```rust,no_run 13 | //! use tuikit::raw::IntoRawMode; 14 | //! use std::io::{Write, stdout}; 15 | //! 16 | //! fn main() { 17 | //! let mut stdout = stdout().into_raw_mode().unwrap(); 18 | //! 19 | //! write!(stdout, "Hey there.").unwrap(); 20 | //! } 21 | //! ``` 22 | 23 | use std::io::{self, Write}; 24 | use std::ops; 25 | 26 | use nix::sys::termios::{cfmakeraw, tcgetattr, tcsetattr, SetArg, Termios}; 27 | use nix::unistd::isatty; 28 | use std::fs; 29 | use std::os::unix::io::{AsRawFd, RawFd}; 30 | 31 | // taken from termion 32 | /// Get the TTY device. 33 | /// 34 | /// This allows for getting stdio representing _only_ the TTY, and not other streams. 35 | pub fn get_tty() -> io::Result { 36 | fs::OpenOptions::new() 37 | .read(true) 38 | .write(true) 39 | .open("/dev/tty") 40 | } 41 | 42 | /// A terminal restorer, which keeps the previous state of the terminal, and restores it, when 43 | /// dropped. 44 | /// 45 | /// Restoring will entirely bring back the old TTY state. 46 | pub struct RawTerminal { 47 | prev_ios: Termios, 48 | output: W, 49 | } 50 | 51 | impl Drop for RawTerminal { 52 | fn drop(&mut self) { 53 | let _ = tcsetattr(self.output.as_raw_fd(), SetArg::TCSANOW, &self.prev_ios); 54 | } 55 | } 56 | 57 | impl ops::Deref for RawTerminal { 58 | type Target = W; 59 | 60 | fn deref(&self) -> &W { 61 | &self.output 62 | } 63 | } 64 | 65 | impl ops::DerefMut for RawTerminal { 66 | fn deref_mut(&mut self) -> &mut W { 67 | &mut self.output 68 | } 69 | } 70 | 71 | impl Write for RawTerminal { 72 | fn write(&mut self, buf: &[u8]) -> io::Result { 73 | self.output.write(buf) 74 | } 75 | 76 | fn flush(&mut self) -> io::Result<()> { 77 | self.output.flush() 78 | } 79 | } 80 | 81 | impl AsRawFd for RawTerminal { 82 | fn as_raw_fd(&self) -> RawFd { 83 | return self.output.as_raw_fd(); 84 | } 85 | } 86 | 87 | /// Types which can be converted into "raw mode". 88 | /// 89 | /// # Why is this type defined on writers and not readers? 90 | /// 91 | /// TTYs has their state controlled by the writer, not the reader. You use the writer to clear the 92 | /// screen, move the cursor and so on, so naturally you use the writer to change the mode as well. 93 | pub trait IntoRawMode: Write + AsRawFd + Sized { 94 | /// Switch to raw mode. 95 | /// 96 | /// Raw mode means that stdin won't be printed (it will instead have to be written manually by 97 | /// the program). Furthermore, the input isn't canonicalised or buffered (that is, you can 98 | /// read from stdin one byte of a time). The output is neither modified in any way. 99 | fn into_raw_mode(self) -> io::Result>; 100 | } 101 | 102 | impl IntoRawMode for W { 103 | // modified after https://github.com/kkawakam/rustyline/blob/master/src/tty/unix.rs#L668 104 | // refer: https://linux.die.net/man/3/termios 105 | fn into_raw_mode(self) -> io::Result> { 106 | use nix::errno::Errno::ENOTTY; 107 | use nix::sys::termios::OutputFlags; 108 | 109 | let istty = isatty(self.as_raw_fd()).map_err(nix_err_to_io_err)?; 110 | if !istty { 111 | Err(nix_err_to_io_err(ENOTTY))? 112 | } 113 | 114 | let prev_ios = tcgetattr(self.as_raw_fd()).map_err(nix_err_to_io_err)?; 115 | let mut ios = prev_ios.clone(); 116 | // set raw mode 117 | cfmakeraw(&mut ios); 118 | // enable output processing (so that '\n' will issue carriage return) 119 | ios.output_flags |= OutputFlags::OPOST; 120 | 121 | tcsetattr(self.as_raw_fd(), SetArg::TCSANOW, &ios).map_err(nix_err_to_io_err)?; 122 | 123 | Ok(RawTerminal { 124 | prev_ios, 125 | output: self, 126 | }) 127 | } 128 | } 129 | 130 | fn nix_err_to_io_err(err: nix::Error) -> io::Error { 131 | io::Error::from(err) 132 | } 133 | -------------------------------------------------------------------------------- /src/widget/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::align::*; 2 | ///! Various pre-defined widget that implements Draw 3 | pub use self::split::*; 4 | pub use self::stack::*; 5 | pub use self::win::*; 6 | use crate::draw::Draw; 7 | use crate::event::Event; 8 | use std::cmp::min; 9 | mod align; 10 | mod split; 11 | mod stack; 12 | mod util; 13 | mod win; 14 | 15 | /// Whether fixed size or percentage 16 | #[derive(Debug, Copy, Clone)] 17 | pub enum Size { 18 | Fixed(usize), 19 | Percent(usize), 20 | Default, 21 | } 22 | 23 | impl Default for Size { 24 | fn default() -> Self { 25 | Size::Default 26 | } 27 | } 28 | 29 | impl Size { 30 | pub fn calc_fixed_size(&self, total_size: usize, default_size: usize) -> usize { 31 | match *self { 32 | Size::Fixed(fixed) => min(total_size, fixed), 33 | Size::Percent(percent) => min(total_size, total_size * percent / 100), 34 | Size::Default => default_size, 35 | } 36 | } 37 | } 38 | 39 | impl From for Size { 40 | fn from(size: usize) -> Self { 41 | Size::Fixed(size) 42 | } 43 | } 44 | 45 | #[derive(Copy, Clone, Debug)] 46 | pub struct Rectangle { 47 | pub top: usize, 48 | pub left: usize, 49 | pub width: usize, 50 | pub height: usize, 51 | } 52 | 53 | impl Rectangle { 54 | /// check if the given point(row, col) lies in the rectangle 55 | pub fn contains(&self, row: usize, col: usize) -> bool { 56 | if row < self.top || row >= self.top + self.height { 57 | false 58 | } else if col < self.left || col >= self.left + self.width { 59 | false 60 | } else { 61 | true 62 | } 63 | } 64 | 65 | /// assume the point (row, col) lies in the rectangle, adjust the origin to the rectangle's 66 | /// origin (top, left) 67 | pub fn relative_to_origin(&self, row: usize, col: usize) -> (usize, usize) { 68 | (row - self.top, col - self.left) 69 | } 70 | 71 | pub fn adjust_origin(&self) -> Rectangle { 72 | Self { 73 | top: 0, 74 | left: 0, 75 | width: self.width, 76 | height: self.height, 77 | } 78 | } 79 | } 80 | 81 | /// A widget could be recursive nested 82 | pub trait Widget: Draw { 83 | /// the (width, height) of the content 84 | /// it will be the hint for layouts to calculate the final size 85 | fn size_hint(&self) -> (Option, Option) { 86 | (None, None) 87 | } 88 | 89 | /// given a key event, emit zero or more messages 90 | /// typical usage is the mouse click event where containers would pass the event down 91 | /// to their children. 92 | fn on_event(&self, event: Event, rect: Rectangle) -> Vec { 93 | let _ = (event, rect); // avoid warning 94 | Vec::new() 95 | } 96 | 97 | /// same as `on_event` except that the self reference is mutable 98 | fn on_event_mut(&mut self, event: Event, rect: Rectangle) -> Vec { 99 | let _ = (event, rect); // avoid warning 100 | Vec::new() 101 | } 102 | } 103 | 104 | impl> Widget for &T { 105 | fn size_hint(&self) -> (Option, Option) { 106 | (*self).size_hint() 107 | } 108 | 109 | fn on_event(&self, event: Event, rect: Rectangle) -> Vec { 110 | (*self).on_event(event, rect) 111 | } 112 | 113 | fn on_event_mut(&mut self, event: Event, rect: Rectangle) -> Vec { 114 | (**self).on_event(event, rect) 115 | } 116 | } 117 | 118 | impl> Widget for &mut T { 119 | fn size_hint(&self) -> (Option, Option) { 120 | (**self).size_hint() 121 | } 122 | 123 | fn on_event(&self, event: Event, rect: Rectangle) -> Vec { 124 | (**self).on_event(event, rect) 125 | } 126 | 127 | fn on_event_mut(&mut self, event: Event, rect: Rectangle) -> Vec { 128 | (**self).on_event_mut(event, rect) 129 | } 130 | } 131 | 132 | impl + ?Sized> Widget for Box { 133 | fn size_hint(&self) -> (Option, Option) { 134 | self.as_ref().size_hint() 135 | } 136 | 137 | fn on_event(&self, event: Event, rect: Rectangle) -> Vec { 138 | self.as_ref().on_event(event, rect) 139 | } 140 | 141 | fn on_event_mut(&mut self, event: Event, rect: Rectangle) -> Vec { 142 | self.as_mut().on_event_mut(event, rect) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/widget/align.rs: -------------------------------------------------------------------------------- 1 | pub trait AlignSelf { 2 | /// say horizontal align, given container's (start, end) and self's size 3 | /// Adjust the actual start position of self. 4 | /// 5 | /// Note that if the container's size < self_size, will return `start` 6 | fn adjust(&self, start: usize, end_exclusive: usize, self_size: usize) -> usize; 7 | } 8 | 9 | pub enum HorizontalAlign { 10 | Left, 11 | Center, 12 | Right, 13 | } 14 | 15 | pub enum VerticalAlign { 16 | Top, 17 | Middle, 18 | Bottom, 19 | } 20 | 21 | impl AlignSelf for HorizontalAlign { 22 | fn adjust(&self, start: usize, end: usize, self_size: usize) -> usize { 23 | if start >= end { 24 | // wrong input 25 | return start; 26 | } 27 | let container_size = end - start; 28 | if container_size <= self_size { 29 | return start; 30 | } 31 | 32 | match self { 33 | HorizontalAlign::Left => start, 34 | HorizontalAlign::Center => start + (container_size - self_size) / 2, 35 | HorizontalAlign::Right => end - self_size, 36 | } 37 | } 38 | } 39 | 40 | impl AlignSelf for VerticalAlign { 41 | fn adjust(&self, start: usize, end: usize, self_size: usize) -> usize { 42 | if start >= end { 43 | // wrong input 44 | return start; 45 | } 46 | let container_size = end - start; 47 | if container_size <= self_size { 48 | return start; 49 | } 50 | 51 | match self { 52 | VerticalAlign::Top => start, 53 | VerticalAlign::Middle => start + (container_size - self_size) / 2, 54 | VerticalAlign::Bottom => end - self_size, 55 | } 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use crate::widget::align::{AlignSelf, HorizontalAlign, VerticalAlign}; 62 | 63 | #[test] 64 | fn size_lt0_return_start() { 65 | assert_eq!(0, HorizontalAlign::Left.adjust(0, 0, 2)); 66 | assert_eq!(0, HorizontalAlign::Center.adjust(0, 0, 2)); 67 | assert_eq!(0, HorizontalAlign::Right.adjust(0, 0, 2)); 68 | assert_eq!(0, VerticalAlign::Top.adjust(0, 0, 2)); 69 | assert_eq!(0, VerticalAlign::Middle.adjust(0, 0, 2)); 70 | assert_eq!(0, VerticalAlign::Bottom.adjust(0, 0, 2)); 71 | 72 | assert_eq!(2, HorizontalAlign::Left.adjust(2, 0, 2)); 73 | assert_eq!(2, HorizontalAlign::Center.adjust(2, 0, 2)); 74 | assert_eq!(2, HorizontalAlign::Right.adjust(2, 0, 2)); 75 | assert_eq!(2, VerticalAlign::Top.adjust(2, 0, 2)); 76 | assert_eq!(2, VerticalAlign::Middle.adjust(2, 0, 2)); 77 | assert_eq!(2, VerticalAlign::Bottom.adjust(2, 0, 2)); 78 | } 79 | 80 | #[test] 81 | fn container_size_too_small_return_start() { 82 | assert_eq!(2, HorizontalAlign::Left.adjust(2, 3, 2)); 83 | assert_eq!(2, HorizontalAlign::Center.adjust(2, 3, 2)); 84 | assert_eq!(2, HorizontalAlign::Right.adjust(2, 3, 2)); 85 | assert_eq!(2, VerticalAlign::Top.adjust(2, 3, 2)); 86 | assert_eq!(2, VerticalAlign::Middle.adjust(2, 3, 2)); 87 | assert_eq!(2, VerticalAlign::Bottom.adjust(2, 3, 2)); 88 | } 89 | 90 | #[test] 91 | fn align_start() { 92 | assert_eq!(2, HorizontalAlign::Left.adjust(2, 8, 2)); 93 | assert_eq!(2, VerticalAlign::Top.adjust(2, 8, 2)); 94 | assert_eq!(2, HorizontalAlign::Left.adjust(2, 7, 2)); 95 | assert_eq!(2, VerticalAlign::Top.adjust(2, 7, 2)); 96 | assert_eq!(2, HorizontalAlign::Left.adjust(2, 8, 3)); 97 | assert_eq!(2, VerticalAlign::Top.adjust(2, 8, 3)); 98 | } 99 | 100 | #[test] 101 | fn align_end() { 102 | assert_eq!(6, HorizontalAlign::Right.adjust(2, 8, 2)); 103 | assert_eq!(6, VerticalAlign::Bottom.adjust(2, 8, 2)); 104 | assert_eq!(5, HorizontalAlign::Right.adjust(2, 7, 2)); 105 | assert_eq!(5, VerticalAlign::Bottom.adjust(2, 7, 2)); 106 | assert_eq!(5, HorizontalAlign::Right.adjust(2, 8, 3)); 107 | assert_eq!(5, VerticalAlign::Bottom.adjust(2, 8, 3)); 108 | } 109 | 110 | #[test] 111 | fn align_center() { 112 | assert_eq!(4, HorizontalAlign::Center.adjust(2, 8, 2)); 113 | assert_eq!(4, VerticalAlign::Middle.adjust(2, 8, 2)); 114 | assert_eq!(3, HorizontalAlign::Center.adjust(2, 7, 2)); 115 | assert_eq!(3, VerticalAlign::Middle.adjust(2, 7, 2)); 116 | assert_eq!(3, HorizontalAlign::Center.adjust(2, 8, 3)); 117 | assert_eq!(3, VerticalAlign::Middle.adjust(2, 8, 3)); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Logs 2 | 3 | ## v0.4.6: 2022-05-04 4 | 5 | Feature: 6 | - parse `alt-space` to `Alt(' ')` 7 | - implement binding of usercase chars(e.g. `shift-x`) 8 | 9 | Fix: 10 | - update `term` to `0.7` 11 | - update `nix` to `0.24.1` 12 | - layout example on README won't compile 13 | 14 | ## v0.4.5: 2021-02-15 15 | 16 | Feature: 17 | - Travis CI -> Github Actions 18 | 19 | Fix: 20 | - parse missing keynames(ctrl-up/down/left/right) 21 | 22 | ## v0.4.4: 2021-02-14 23 | 24 | Feature: 25 | - tuikit now returns concrete errors 26 | 27 | Fix: 28 | - restore the `clear_on_exit` behavior 29 | - key listener no longer quit(hang) on unknown sequence 30 | 31 | ## v0.4.3: 2021-01-03 32 | 33 | Feature: 34 | - support bracketed paste mode 35 | 36 | ## v0.4.2: 2020-10-20 37 | 38 | Fix: 39 | - click/wheel events' row were not adjusted in non-fullscreen mode 40 | 41 | ## v0.4.1: 2020-10-18 42 | 43 | Fix: 44 | - `Term` not paused on drop. 45 | 46 | ## v0.4.0: 2020-10-15 47 | 48 | Feature: 49 | - support `hold` option that don't start term on creation. 50 | - support user defined event. 51 | - unify result types 52 | 53 | ## v0.3.4: 2020-10-06 54 | 55 | Feature: 56 | - widget `win` support header and right prompt 57 | - new widget: `stack` for stacking widget bottom up 58 | - keyboard now parses double click events 59 | - in this mode, `MousePress` event would no longer be generated 60 | - keyboard now merges consecutive wheel events 61 | 62 | Fix: 63 | - show cursor when quiting alternate screen 64 | 65 | ## v0.3.3: 2020-06-26 66 | 67 | - fix [skim#308](https://github.com/lotabout/skim/issues/308): skim hang on 68 | initialization 69 | 70 | ## v0.3.2: 2020-04-01 71 | 72 | - fix skim#259 release lock correctly on pause 73 | - fix skim#277: x10 mouse event was capped 74 | 75 | ## v0.3.1: 2020-02-05 76 | 77 | - fix skim#232: use `cfmakeraw` to enable raw mode 78 | - fix build with rust 1.32.0 79 | 80 | ## v0.3.0: 2020-01-30 81 | 82 | Feature: 83 | - Feature: option to clear screen or not after exit. 84 | - Feature: new trait `Widget` 85 | 86 | Bug fixes: 87 | - fix skim#255: parse `space` as key ` ` 88 | - reset mouse status before exit. 89 | - fix: adjust mouse position(row)'s origin 90 | 91 | Examples: 92 | - 256color_on_screen: reset attributes before flush 93 | - fix #10: output help in split example 94 | - get_keys: disable mouse before existing 95 | - all: make examples quit on Ctrl-C 96 | 97 | Depedency Update: 98 | - `term` to `0.6` 99 | 100 | ## v0.2.9: 2019-07-28 101 | 102 | Fix: [skim#192](https://github.com/lotabout/skim/issues/192): Start drawing in 103 | a clean line. 104 | 105 | ## v0.2.8: 2019-06-05 106 | 107 | Update dependenncy `nix` to `0.14`. 108 | 109 | ## v0.2.7: 2019-06-04 110 | 111 | Features: 112 | - Implement `From` trait for variaous struct 113 | * `From for Attr` 114 | * `From for Attr` 115 | * `From for Cell` 116 | - `win/split` now accept `Into<...>` struct. Previously when initializing 117 | splits, you need to write `split.basis(10.into())`, 118 | now it's just `split.basis(10)`. 119 | - Implement builder pattern for `Attr`. We could now do 120 | `Attr::default().fg(...).bg(...)`. 121 | - Add two user defined event(`User1` and `User2`). Use it for your own need. 122 | 123 | Bug fixes: 124 | - fix compilation error on FreeBSD. 125 | 126 | ## v0.2.6: 2019-03-28 127 | 128 | Reduce CPU usage on idle. 129 | 130 | ## v0.2.5: 2019-03-28 131 | 132 | Clear screen on resize 133 | 134 | ## v0.2.4: 2019-03-22 135 | 136 | Fix: ESC key not working 137 | 138 | ## v0.2.3: 2019-03-23 139 | 140 | - Support more alt keys 141 | - impl `Draw` for `Box` 142 | 143 | ## v0.2.2: 2019-03-19 144 | 145 | API change: `Draw::content_size` -> `Draw::size_hint` and returns 146 | `Option`. So that `None` could indicates "I don't know". 147 | 148 | ## v0.2.1: 2019-03-17 149 | 150 | - fix: build failed with rust 2018 (1.31.0) 151 | 152 | ## v0.2.0: 2019-03-17 153 | 154 | Feature: 155 | - Support layout(e.g. `HSplit`, `VSplit`) 156 | - `term.send_event` to inject event to `Term`'s event loop 157 | - `use tuikit::prelude::*` to simplify import 158 | 159 | ## v0.1.5: 2019-03-02 160 | 161 | Fix: Synchronize the pause and restart event. 162 | 163 | ## v0.1.4: 2019-02-25 164 | 165 | Fix: output will replace raw ESC(`\x1b`) with `?` so that terminal won't mess up. 166 | 167 | ## v0.1.3: 2019-02-24 168 | 169 | Fix: report cursor position (0, 0) on terminals that doesn't support CPR. 170 | 171 | ## v0.1.2: 2019-02-24 172 | 173 | Features: 174 | - support specifying `min-height` and `max-height` 175 | - screen: able to iterate over all cells 176 | - attr: add `extend` method for composing `Attr`. 177 | 178 | Bug Fixes: 179 | - #1 Increase timeout(to 300ms) on initialize to support slow terminals 180 | - #3 erase contents on exit 181 | - screen: fix panic on height/width of `0` 182 | - fix some key parsing error 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [DEPRECATED] This code has been migrated to [skim](https://github.com/skim-rs/skim/tuikit). See https://github.com/skim-rs/skim?tab=readme-ov-file#tuikit for how to access its member. 2 | 3 | 4 | 5 | [![Crates.io](https://img.shields.io/crates/v/tuikit.svg)](https://crates.io/crates/tuikit) [![Build Status](https://github.com/lotabout/tuikit/workflows/Build%20&%20Test/badge.svg)](https://github.com/lotabout/tuikit/actions?query=workflow%3A%22Build+%26+Test%22) 6 | 7 | ## Tuikit 8 | 9 | Tuikit is a TUI library for writing terminal UI applications. Highlights: 10 | 11 | - Thread safe. 12 | - Support non-fullscreen mode as well as fullscreen mode. 13 | - Support `Alt` keys, mouse events, etc. 14 | - Buffering for efficient rendering. 15 | 16 | Tuikit is modeld after [termbox](https://github.com/nsf/termbox) which views the 17 | terminal as a table of fixed-size cells and input being a stream of structured 18 | messages. 19 | 20 | **WARNING**: The library is not stable yet, the API might change. 21 | 22 | ## Usage 23 | 24 | In your `Cargo.toml` add the following: 25 | 26 | ```toml 27 | [dependencies] 28 | tuikit = "*" 29 | ``` 30 | 31 | And if you'd like to use the latest snapshot version: 32 | 33 | ```toml 34 | [dependencies] 35 | tuikit = { git = "https://github.com/lotabout/tuikit.git" } 36 | ``` 37 | 38 | Here is an example (could also be run by `cargo run --example hello-world`): 39 | 40 | ```rust 41 | use tuikit::prelude::*; 42 | use std::cmp::{min, max}; 43 | 44 | fn main() { 45 | let term: Term<()> = Term::with_height(TermHeight::Percent(30)).unwrap(); 46 | let mut row = 1; 47 | let mut col = 0; 48 | 49 | let _ = term.print(0, 0, "press arrow key to move the text, (q) to quit"); 50 | let _ = term.present(); 51 | 52 | while let Ok(ev) = term.poll_event() { 53 | let _ = term.clear(); 54 | let _ = term.print(0, 0, "press arrow key to move the text, (q) to quit"); 55 | 56 | let (width, height) = term.term_size().unwrap(); 57 | match ev { 58 | Event::Key(Key::ESC) | Event::Key(Key::Char('q')) => break, 59 | Event::Key(Key::Up) => row = max(row-1, 1), 60 | Event::Key(Key::Down) => row = min(row+1, height-1), 61 | Event::Key(Key::Left) => col = max(col, 1)-1, 62 | Event::Key(Key::Right) => col = min(col+1, width-1), 63 | _ => {} 64 | } 65 | 66 | let attr = Attr{ fg: Color::RED, ..Attr::default() }; 67 | let _ = term.print_with_attr(row, col, "Hello World! 你好!今日は。", attr); 68 | let _ = term.set_cursor(row, col); 69 | let _ = term.present(); 70 | } 71 | } 72 | ``` 73 | 74 | ## Layout 75 | 76 | `tuikit` provides `HSplit`, `VSplit` and `Win` for managing layouts: 77 | 78 | 1. `HSplit` allow you to split area horizontally into pieces. 79 | 2. `VSplit` works just like `HSplit` but splits vertically. 80 | 3. `Win` do not split, it could have margin, padding and border. 81 | 82 | For example: 83 | 84 | ```rust 85 | use tuikit::prelude::*; 86 | 87 | struct Model(String); 88 | 89 | impl Draw for Model { 90 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 91 | let (width, height) = canvas.size()?; 92 | let message_width = self.0.len(); 93 | let left = (width - message_width) / 2; 94 | let top = height / 2; 95 | let _ = canvas.print(top, left, &self.0); 96 | Ok(()) 97 | } 98 | } 99 | 100 | impl Widget for Model{} 101 | 102 | fn main() { 103 | let term: Term<()> = Term::with_height(TermHeight::Percent(50)).unwrap(); 104 | let model = Model("middle!".to_string()); 105 | 106 | while let Ok(ev) = term.poll_event() { 107 | if let Event::Key(Key::Char('q')) = ev { 108 | break; 109 | } 110 | let _ = term.print(0, 0, "press 'q' to exit"); 111 | 112 | let hsplit = HSplit::default() 113 | .split( 114 | VSplit::default() 115 | .basis(Size::Percent(30)) 116 | .split(Win::new(&model).border(true).basis(Size::Percent(30))) 117 | .split(Win::new(&model).border(true).basis(Size::Percent(30))) 118 | ) 119 | .split(Win::new(&model).border(true)); 120 | 121 | let _ = term.draw(&hsplit); 122 | let _ = term.present(); 123 | } 124 | } 125 | ``` 126 | 127 | The split algorithm is simple: 128 | 129 | 1. Both `HSplit` and `VSplit` will take several `Split` where a `Split` would 130 | contains: 131 | 1. basis, the original size 132 | 2. grow, the factor to grow if there is still enough room 133 | 3. shrink, the factor to shrink if there is not enough room 134 | 2. `HSplit/VSplit` will count the total width/height(basis) of the split items 135 | 3. Judge if the current width/height is enough or not for the split items 136 | 4. shrink/grow the split items according to their grow/shrink: `factor / sum(factors)` 137 | 5. If still not enough room, the last one(s) would be set width/height 0 138 | 139 | ## References 140 | 141 | `Tuikit` borrows ideas from lots of other projects: 142 | 143 | - [rustyline](https://github.com/kkawakam/rustyline) Readline Implementation in Rust. 144 | - How to enter the raw mode. 145 | - Part of the keycode parsing logic. 146 | - [termion](https://gitlab.redox-os.org/redox-os/termion) A bindless library for controlling terminals/TTY. 147 | - How to parse mouse events. 148 | - How to enter raw mode. 149 | - [rustbox](https://github.com/gchp/rustbox) and [termbox](https://github.com/nsf/termbox) 150 | - The idea of viewing terminal as table of fixed cells. 151 | - [termfest](https://github.com/agatan/termfest) Easy TUI library written in Rust 152 | - The buffering idea. 153 | -------------------------------------------------------------------------------- /src/widget/stack.rs: -------------------------------------------------------------------------------- 1 | use crate::canvas::Canvas; 2 | use crate::draw::{Draw, DrawResult}; 3 | use crate::event::Event; 4 | use crate::widget::{Rectangle, Widget}; 5 | 6 | /// A stack of widgets, will draw the including widgets back to front 7 | pub struct Stack<'a, Message = ()> { 8 | inner: Vec + 'a>>, 9 | } 10 | 11 | impl<'a, Message> Stack<'a, Message> { 12 | pub fn new() -> Self { 13 | Self { inner: vec![] } 14 | } 15 | 16 | pub fn top(mut self, widget: impl Widget + 'a) -> Self { 17 | self.inner.push(Box::new(widget)); 18 | self 19 | } 20 | 21 | pub fn bottom(mut self, widget: impl Widget + 'a) -> Self { 22 | self.inner.insert(0, Box::new(widget)); 23 | self 24 | } 25 | } 26 | 27 | impl<'a, Message> Draw for Stack<'a, Message> { 28 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 29 | for widget in self.inner.iter() { 30 | widget.draw(canvas)? 31 | } 32 | 33 | Ok(()) 34 | } 35 | fn draw_mut(&mut self, canvas: &mut dyn Canvas) -> DrawResult<()> { 36 | for widget in self.inner.iter_mut() { 37 | widget.draw_mut(canvas)? 38 | } 39 | 40 | Ok(()) 41 | } 42 | } 43 | 44 | impl<'a, Message> Widget for Stack<'a, Message> { 45 | fn size_hint(&self) -> (Option, Option) { 46 | // max of the inner widgets 47 | let width = self 48 | .inner 49 | .iter() 50 | .map(|widget| widget.size_hint().0) 51 | .max() 52 | .unwrap_or(None); 53 | let height = self 54 | .inner 55 | .iter() 56 | .map(|widget| widget.size_hint().1) 57 | .max() 58 | .unwrap_or(None); 59 | (width, height) 60 | } 61 | 62 | fn on_event(&self, event: Event, rect: Rectangle) -> Vec { 63 | // like javascript's capture, from top to bottom 64 | for widget in self.inner.iter().rev() { 65 | let message = widget.on_event(event, rect); 66 | if !message.is_empty() { 67 | return message; 68 | } 69 | } 70 | vec![] 71 | } 72 | 73 | fn on_event_mut(&mut self, event: Event, rect: Rectangle) -> Vec { 74 | // like javascript's capture, from top to bottom 75 | for widget in self.inner.iter_mut().rev() { 76 | let message = widget.on_event_mut(event, rect); 77 | if !message.is_empty() { 78 | return message; 79 | } 80 | } 81 | vec![] 82 | } 83 | } 84 | 85 | #[cfg(test)] 86 | #[allow(dead_code)] 87 | mod test { 88 | use super::*; 89 | use crate::cell::Cell; 90 | use std::sync::Mutex; 91 | 92 | struct WinHint { 93 | pub width_hint: Option, 94 | pub height_hint: Option, 95 | } 96 | 97 | impl Draw for WinHint { 98 | fn draw(&self, _canvas: &mut dyn Canvas) -> DrawResult<()> { 99 | unimplemented!() 100 | } 101 | } 102 | 103 | impl Widget for WinHint { 104 | fn size_hint(&self) -> (Option, Option) { 105 | (self.width_hint, self.height_hint) 106 | } 107 | } 108 | 109 | #[test] 110 | fn size_hint() { 111 | let stack = Stack::new().top(WinHint { 112 | width_hint: None, 113 | height_hint: None, 114 | }); 115 | assert_eq!((None, None), stack.size_hint()); 116 | 117 | let stack = Stack::new().top(WinHint { 118 | width_hint: Some(1), 119 | height_hint: Some(1), 120 | }); 121 | assert_eq!((Some(1), Some(1)), stack.size_hint()); 122 | 123 | let stack = Stack::new() 124 | .top(WinHint { 125 | width_hint: Some(1), 126 | height_hint: Some(2), 127 | }) 128 | .top(WinHint { 129 | width_hint: Some(2), 130 | height_hint: Some(1), 131 | }); 132 | assert_eq!((Some(2), Some(2)), stack.size_hint()); 133 | 134 | let stack = Stack::new() 135 | .top(WinHint { 136 | width_hint: None, 137 | height_hint: None, 138 | }) 139 | .top(WinHint { 140 | width_hint: Some(2), 141 | height_hint: Some(1), 142 | }); 143 | assert_eq!((Some(2), Some(1)), stack.size_hint()); 144 | } 145 | 146 | #[derive(PartialEq, Debug)] 147 | enum Called { 148 | No, 149 | Mut, 150 | Immut, 151 | } 152 | 153 | struct Drawn { 154 | called: Mutex, 155 | } 156 | 157 | impl Draw for Drawn { 158 | fn draw(&self, _canvas: &mut dyn Canvas) -> DrawResult<()> { 159 | *self.called.lock().unwrap() = Called::Immut; 160 | Ok(()) 161 | } 162 | fn draw_mut(&mut self, _canvas: &mut dyn Canvas) -> DrawResult<()> { 163 | *self.called.lock().unwrap() = Called::Mut; 164 | Ok(()) 165 | } 166 | } 167 | 168 | impl Widget for Drawn {} 169 | 170 | #[derive(Default)] 171 | struct TestCanvas {} 172 | 173 | #[allow(unused_variables)] 174 | impl Canvas for TestCanvas { 175 | fn size(&self) -> crate::Result<(usize, usize)> { 176 | Ok((100, 100)) 177 | } 178 | 179 | fn clear(&mut self) -> crate::Result<()> { 180 | unimplemented!() 181 | } 182 | 183 | fn put_cell(&mut self, row: usize, col: usize, cell: Cell) -> crate::Result { 184 | Ok(1) 185 | } 186 | 187 | fn set_cursor(&mut self, row: usize, col: usize) -> crate::Result<()> { 188 | unimplemented!() 189 | } 190 | 191 | fn show_cursor(&mut self, show: bool) -> crate::Result<()> { 192 | unimplemented!() 193 | } 194 | } 195 | 196 | #[test] 197 | fn mutable_widget() { 198 | let mut canvas = TestCanvas::default(); 199 | 200 | let mut mutable = Drawn { 201 | called: Mutex::new(Called::No), 202 | }; 203 | { 204 | let mut stack = Stack::new().top(&mut mutable); 205 | let _ = stack.draw_mut(&mut canvas).unwrap(); 206 | } 207 | assert_eq!(Called::Mut, *mutable.called.lock().unwrap()); 208 | 209 | let immutable = Drawn { 210 | called: Mutex::new(Called::No), 211 | }; 212 | let stack = Stack::new().top(&immutable); 213 | let _ = stack.draw(&mut canvas).unwrap(); 214 | assert_eq!(Called::Immut, *immutable.called.lock().unwrap()); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "atty" 16 | version = "0.2.14" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 19 | dependencies = [ 20 | "hermit-abi", 21 | "libc", 22 | "winapi", 23 | ] 24 | 25 | [[package]] 26 | name = "bitflags" 27 | version = "1.2.1" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 30 | 31 | [[package]] 32 | name = "cfg-if" 33 | version = "0.1.10" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 36 | 37 | [[package]] 38 | name = "cfg-if" 39 | version = "1.0.0" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 42 | 43 | [[package]] 44 | name = "dirs-next" 45 | version = "2.0.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 48 | dependencies = [ 49 | "cfg-if 1.0.0", 50 | "dirs-sys-next", 51 | ] 52 | 53 | [[package]] 54 | name = "dirs-sys-next" 55 | version = "0.1.2" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 58 | dependencies = [ 59 | "libc", 60 | "redox_users", 61 | "winapi", 62 | ] 63 | 64 | [[package]] 65 | name = "env_logger" 66 | version = "0.6.2" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" 69 | dependencies = [ 70 | "atty", 71 | "humantime", 72 | "log", 73 | "regex", 74 | "termcolor", 75 | ] 76 | 77 | [[package]] 78 | name = "getrandom" 79 | version = "0.2.2" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" 82 | dependencies = [ 83 | "cfg-if 1.0.0", 84 | "libc", 85 | "wasi", 86 | ] 87 | 88 | [[package]] 89 | name = "hermit-abi" 90 | version = "0.1.17" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" 93 | dependencies = [ 94 | "libc", 95 | ] 96 | 97 | [[package]] 98 | name = "humantime" 99 | version = "1.3.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 102 | dependencies = [ 103 | "quick-error", 104 | ] 105 | 106 | [[package]] 107 | name = "lazy_static" 108 | version = "1.4.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 111 | 112 | [[package]] 113 | name = "libc" 114 | version = "0.2.125" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" 117 | 118 | [[package]] 119 | name = "log" 120 | version = "0.4.11" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" 123 | dependencies = [ 124 | "cfg-if 0.1.10", 125 | ] 126 | 127 | [[package]] 128 | name = "memchr" 129 | version = "2.5.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 132 | 133 | [[package]] 134 | name = "nix" 135 | version = "0.24.1" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "8f17df307904acd05aa8e32e97bb20f2a0df1728bbc2d771ae8f9a90463441e9" 138 | dependencies = [ 139 | "bitflags", 140 | "cfg-if 1.0.0", 141 | "libc", 142 | ] 143 | 144 | [[package]] 145 | name = "quick-error" 146 | version = "1.2.3" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 149 | 150 | [[package]] 151 | name = "redox_syscall" 152 | version = "0.2.5" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" 155 | dependencies = [ 156 | "bitflags", 157 | ] 158 | 159 | [[package]] 160 | name = "redox_users" 161 | version = "0.4.0" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" 164 | dependencies = [ 165 | "getrandom", 166 | "redox_syscall", 167 | ] 168 | 169 | [[package]] 170 | name = "regex" 171 | version = "1.5.6" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" 174 | dependencies = [ 175 | "aho-corasick", 176 | "memchr", 177 | "regex-syntax", 178 | ] 179 | 180 | [[package]] 181 | name = "regex-syntax" 182 | version = "0.6.26" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" 185 | 186 | [[package]] 187 | name = "rustversion" 188 | version = "1.0.4" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "cb5d2a036dc6d2d8fd16fde3498b04306e29bd193bf306a57427019b823d5acd" 191 | 192 | [[package]] 193 | name = "term" 194 | version = "0.7.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" 197 | dependencies = [ 198 | "dirs-next", 199 | "rustversion", 200 | "winapi", 201 | ] 202 | 203 | [[package]] 204 | name = "termcolor" 205 | version = "1.1.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" 208 | dependencies = [ 209 | "winapi-util", 210 | ] 211 | 212 | [[package]] 213 | name = "tuikit" 214 | version = "0.5.0" 215 | dependencies = [ 216 | "bitflags", 217 | "env_logger", 218 | "lazy_static", 219 | "log", 220 | "nix", 221 | "term", 222 | "unicode-width", 223 | ] 224 | 225 | [[package]] 226 | name = "unicode-width" 227 | version = "0.1.8" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 230 | 231 | [[package]] 232 | name = "wasi" 233 | version = "0.10.2+wasi-snapshot-preview1" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 236 | 237 | [[package]] 238 | name = "winapi" 239 | version = "0.3.9" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 242 | dependencies = [ 243 | "winapi-i686-pc-windows-gnu", 244 | "winapi-x86_64-pc-windows-gnu", 245 | ] 246 | 247 | [[package]] 248 | name = "winapi-i686-pc-windows-gnu" 249 | version = "0.4.0" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 252 | 253 | [[package]] 254 | name = "winapi-util" 255 | version = "0.1.5" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 258 | dependencies = [ 259 | "winapi", 260 | ] 261 | 262 | [[package]] 263 | name = "winapi-x86_64-pc-windows-gnu" 264 | version = "0.4.0" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 267 | -------------------------------------------------------------------------------- /src/key.rs: -------------------------------------------------------------------------------- 1 | //! Defines all the keys `tuikit` recognizes. 2 | 3 | // http://ascii-table.com/ansi-escape-sequences.php 4 | /// Single key 5 | #[rustfmt::skip] 6 | #[derive(Eq, PartialEq, Hash, Debug, Copy, Clone)] 7 | pub enum Key { 8 | Null, 9 | ESC, 10 | 11 | Ctrl(char), 12 | Tab, // Ctrl-I 13 | Enter, // Ctrl-M 14 | 15 | BackTab, Backspace, AltBackTab, 16 | 17 | Up, Down, Left, Right, Home, End, Insert, Delete, PageUp, PageDown, 18 | CtrlUp, CtrlDown, CtrlLeft, CtrlRight, 19 | ShiftUp, ShiftDown, ShiftLeft, ShiftRight, 20 | AltUp, AltDown, AltLeft, AltRight, AltHome, AltEnd, AltPageUp, AltPageDown, 21 | AltShiftUp, AltShiftDown, AltShiftLeft, AltShiftRight, 22 | 23 | F(u8), 24 | 25 | CtrlAlt(char), // chars are lower case 26 | AltEnter, 27 | AltBackspace, 28 | AltTab, 29 | Alt(char), // chars could be lower or upper case 30 | Char(char), // chars could be lower or upper case 31 | CursorPos(u16, u16), // row, col 32 | 33 | // raw mouse events, will only generated if raw mouse mode is enabled 34 | MousePress(MouseButton, u16, u16), // row, col 35 | MouseRelease(u16, u16), // row, col 36 | MouseHold(u16, u16), // row, col 37 | 38 | // parsed mouse events, will be generated if raw mouse mode is disabled 39 | SingleClick(MouseButton, u16, u16), // row, col 40 | DoubleClick(MouseButton, u16, u16), // row, col, will only record left button double click 41 | WheelUp(u16, u16, u16), // row, col, number of scroll 42 | WheelDown(u16, u16, u16), // row, col, number of scroll 43 | 44 | BracketedPasteStart, 45 | BracketedPasteEnd, 46 | 47 | #[doc(hidden)] 48 | __Nonexhaustive, 49 | 50 | } 51 | 52 | /// A mouse button. 53 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 54 | pub enum MouseButton { 55 | /// The left mouse button. 56 | Left, 57 | /// The right mouse button. 58 | Right, 59 | /// The middle mouse button. 60 | Middle, 61 | /// Mouse wheel is going up. 62 | /// 63 | /// This event is typically only used with MousePress. 64 | WheelUp, 65 | /// Mouse wheel is going down. 66 | /// 67 | /// This event is typically only used with MousePress. 68 | WheelDown, 69 | } 70 | 71 | #[rustfmt::skip] 72 | pub fn from_keyname(keyname: &str) -> Option { 73 | use self::Key::*; 74 | match keyname.to_lowercase().as_ref() { 75 | "ctrl-space" | "ctrl-`" | "ctrl-@" => Some(Ctrl(' ')), 76 | "ctrl-a" => Some(Ctrl('a')), 77 | "ctrl-b" => Some(Ctrl('b')), 78 | "ctrl-c" => Some(Ctrl('c')), 79 | "ctrl-d" => Some(Ctrl('d')), 80 | "ctrl-e" => Some(Ctrl('e')), 81 | "ctrl-f" => Some(Ctrl('f')), 82 | "ctrl-g" => Some(Ctrl('g')), 83 | "ctrl-h" => Some(Ctrl('h')), 84 | "tab" | "ctrl-i" => Some(Tab), 85 | "ctrl-j" => Some(Ctrl('j')), 86 | "ctrl-k" => Some(Ctrl('k')), 87 | "ctrl-l" => Some(Ctrl('l')), 88 | "enter" | "return" | "ctrl-m" => Some(Enter), 89 | "ctrl-n" => Some(Ctrl('n')), 90 | "ctrl-o" => Some(Ctrl('o')), 91 | "ctrl-p" => Some(Ctrl('p')), 92 | "ctrl-q" => Some(Ctrl('q')), 93 | "ctrl-r" => Some(Ctrl('r')), 94 | "ctrl-s" => Some(Ctrl('s')), 95 | "ctrl-t" => Some(Ctrl('t')), 96 | "ctrl-u" => Some(Ctrl('u')), 97 | "ctrl-v" => Some(Ctrl('v')), 98 | "ctrl-w" => Some(Ctrl('w')), 99 | "ctrl-x" => Some(Ctrl('x')), 100 | "ctrl-y" => Some(Ctrl('y')), 101 | "ctrl-z" => Some(Ctrl('z')), 102 | "ctrl-up" => Some(CtrlUp), 103 | "ctrl-down" => Some(CtrlDown), 104 | "ctrl-left" => Some(CtrlLeft), 105 | "ctrl-right" => Some(CtrlRight), 106 | 107 | "ctrl-alt-space" => Some(Ctrl(' ')), 108 | "ctrl-alt-a" => Some(CtrlAlt('a')), 109 | "ctrl-alt-b" => Some(CtrlAlt('b')), 110 | "ctrl-alt-c" => Some(CtrlAlt('c')), 111 | "ctrl-alt-d" => Some(CtrlAlt('d')), 112 | "ctrl-alt-e" => Some(CtrlAlt('e')), 113 | "ctrl-alt-f" => Some(CtrlAlt('f')), 114 | "ctrl-alt-g" => Some(CtrlAlt('g')), 115 | "ctrl-alt-h" => Some(CtrlAlt('h')), 116 | "ctrl-alt-j" => Some(CtrlAlt('j')), 117 | "ctrl-alt-k" => Some(CtrlAlt('k')), 118 | "ctrl-alt-l" => Some(CtrlAlt('l')), 119 | "ctrl-alt-n" => Some(CtrlAlt('n')), 120 | "ctrl-alt-o" => Some(CtrlAlt('o')), 121 | "ctrl-alt-p" => Some(CtrlAlt('p')), 122 | "ctrl-alt-q" => Some(CtrlAlt('q')), 123 | "ctrl-alt-r" => Some(CtrlAlt('r')), 124 | "ctrl-alt-s" => Some(CtrlAlt('s')), 125 | "ctrl-alt-t" => Some(CtrlAlt('t')), 126 | "ctrl-alt-u" => Some(CtrlAlt('u')), 127 | "ctrl-alt-v" => Some(CtrlAlt('v')), 128 | "ctrl-alt-w" => Some(CtrlAlt('w')), 129 | "ctrl-alt-x" => Some(CtrlAlt('x')), 130 | "ctrl-alt-y" => Some(CtrlAlt('y')), 131 | "ctrl-alt-z" => Some(CtrlAlt('z')), 132 | 133 | "esc" => Some(ESC), 134 | "btab" | "shift-tab" => Some(BackTab), 135 | "bspace" | "bs" => Some(Backspace), 136 | "ins" | "insert" => Some(Insert), 137 | "del" => Some(Delete), 138 | "pgup" | "page-up" => Some(PageUp), 139 | "pgdn" | "page-down" => Some(PageDown), 140 | "up" => Some(Up), 141 | "down" => Some(Down), 142 | "left" => Some(Left), 143 | "right" => Some(Right), 144 | "home" => Some(Home), 145 | "end" => Some(End), 146 | "shift-up" => Some(ShiftUp), 147 | "shift-down" => Some(ShiftDown), 148 | "shift-left" => Some(ShiftLeft), 149 | "shift-right" => Some(ShiftRight), 150 | 151 | "f1" => Some(F(1)), 152 | "f2" => Some(F(2)), 153 | "f3" => Some(F(3)), 154 | "f4" => Some(F(4)), 155 | "f5" => Some(F(5)), 156 | "f6" => Some(F(6)), 157 | "f7" => Some(F(7)), 158 | "f8" => Some(F(8)), 159 | "f9" => Some(F(9)), 160 | "f10" => Some(F(10)), 161 | "f11" => Some(F(11)), 162 | "f12" => Some(F(12)), 163 | 164 | "alt-a" => Some(Alt('a')), 165 | "alt-b" => Some(Alt('b')), 166 | "alt-c" => Some(Alt('c')), 167 | "alt-d" => Some(Alt('d')), 168 | "alt-e" => Some(Alt('e')), 169 | "alt-f" => Some(Alt('f')), 170 | "alt-g" => Some(Alt('g')), 171 | "alt-h" => Some(Alt('h')), 172 | "alt-i" => Some(Alt('i')), 173 | "alt-j" => Some(Alt('j')), 174 | "alt-k" => Some(Alt('k')), 175 | "alt-l" => Some(Alt('l')), 176 | "alt-m" => Some(Alt('m')), 177 | "alt-n" => Some(Alt('n')), 178 | "alt-o" => Some(Alt('o')), 179 | "alt-p" => Some(Alt('p')), 180 | "alt-q" => Some(Alt('q')), 181 | "alt-r" => Some(Alt('r')), 182 | "alt-s" => Some(Alt('s')), 183 | "alt-t" => Some(Alt('t')), 184 | "alt-u" => Some(Alt('u')), 185 | "alt-v" => Some(Alt('v')), 186 | "alt-w" => Some(Alt('w')), 187 | "alt-x" => Some(Alt('x')), 188 | "alt-y" => Some(Alt('y')), 189 | "alt-z" => Some(Alt('z')), 190 | "alt-/" => Some(Alt('/')), 191 | 192 | "shift-a" => Some(Char('A')), 193 | "shift-b" => Some(Char('B')), 194 | "shift-c" => Some(Char('C')), 195 | "shift-d" => Some(Char('D')), 196 | "shift-e" => Some(Char('E')), 197 | "shift-f" => Some(Char('F')), 198 | "shift-g" => Some(Char('G')), 199 | "shift-h" => Some(Char('H')), 200 | "shift-i" => Some(Char('I')), 201 | "shift-j" => Some(Char('J')), 202 | "shift-k" => Some(Char('K')), 203 | "shift-l" => Some(Char('L')), 204 | "shift-m" => Some(Char('M')), 205 | "shift-n" => Some(Char('N')), 206 | "shift-o" => Some(Char('O')), 207 | "shift-p" => Some(Char('P')), 208 | "shift-q" => Some(Char('Q')), 209 | "shift-r" => Some(Char('R')), 210 | "shift-s" => Some(Char('S')), 211 | "shift-t" => Some(Char('T')), 212 | "shift-u" => Some(Char('U')), 213 | "shift-v" => Some(Char('V')), 214 | "shift-w" => Some(Char('W')), 215 | "shift-x" => Some(Char('X')), 216 | "shift-y" => Some(Char('Y')), 217 | "shift-z" => Some(Char('Z')), 218 | 219 | "alt-shift-a" => Some(Alt('A')), 220 | "alt-shift-b" => Some(Alt('B')), 221 | "alt-shift-c" => Some(Alt('C')), 222 | "alt-shift-d" => Some(Alt('D')), 223 | "alt-shift-e" => Some(Alt('E')), 224 | "alt-shift-f" => Some(Alt('F')), 225 | "alt-shift-g" => Some(Alt('G')), 226 | "alt-shift-h" => Some(Alt('H')), 227 | "alt-shift-i" => Some(Alt('I')), 228 | "alt-shift-j" => Some(Alt('J')), 229 | "alt-shift-k" => Some(Alt('K')), 230 | "alt-shift-l" => Some(Alt('L')), 231 | "alt-shift-m" => Some(Alt('M')), 232 | "alt-shift-n" => Some(Alt('N')), 233 | "alt-shift-o" => Some(Alt('O')), 234 | "alt-shift-p" => Some(Alt('P')), 235 | "alt-shift-q" => Some(Alt('Q')), 236 | "alt-shift-r" => Some(Alt('R')), 237 | "alt-shift-s" => Some(Alt('S')), 238 | "alt-shift-t" => Some(Alt('T')), 239 | "alt-shift-u" => Some(Alt('U')), 240 | "alt-shift-v" => Some(Alt('V')), 241 | "alt-shift-w" => Some(Alt('W')), 242 | "alt-shift-x" => Some(Alt('X')), 243 | "alt-shift-y" => Some(Alt('Y')), 244 | "alt-shift-z" => Some(Alt('Z')), 245 | 246 | "alt-btab" | "alt-shift-tab" => Some(AltBackTab), 247 | "alt-bspace" | "alt-bs" => Some(AltBackspace), 248 | "alt-pgup" | "alt-page-up" => Some(AltPageUp), 249 | "alt-pgdn" | "alt-page-down" => Some(AltPageDown), 250 | "alt-up" => Some(AltUp), 251 | "alt-down" => Some(AltDown), 252 | "alt-left" => Some(AltLeft), 253 | "alt-right" => Some(AltRight), 254 | "alt-home" => Some(AltHome), 255 | "alt-end" => Some(AltEnd), 256 | "alt-shift-up" => Some(AltShiftUp), 257 | "alt-shift-down" => Some(AltShiftDown), 258 | "alt-shift-left" => Some(AltShiftLeft), 259 | "alt-shift-right" => Some(AltShiftRight), 260 | "alt-enter" | "alt-ctrl-m" => Some(AltEnter), 261 | "alt-tab" | "alt-ctrl-i" => Some(AltTab), 262 | 263 | "space" => Some(Char(' ')), 264 | "alt-space" => Some(Alt(' ')), 265 | 266 | ch if ch.chars().count() == 1 => { 267 | Some(Char(ch.chars().next().expect("input:parse_key: no key is specified"))) 268 | }, 269 | _ => None, 270 | } 271 | } 272 | 273 | #[cfg(test)] 274 | mod test { 275 | use super::Key::*; 276 | use super::*; 277 | 278 | #[test] 279 | fn bind_shift_key() { 280 | // Without the "shift-" prefix, "from_keyname" ignores the case. 281 | assert_eq!(from_keyname("A").unwrap(), Char('a')); 282 | 283 | // A correct way to refer to an uppercase char. 284 | assert_eq!(from_keyname("shift-a").unwrap(), Char('A')); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/screen.rs: -------------------------------------------------------------------------------- 1 | //! Buffering screen cells and try to optimize rendering contents 2 | use crate::attr::Attr; 3 | use crate::canvas::Canvas; 4 | use crate::cell::Cell; 5 | use crate::error::TuikitError; 6 | use crate::output::Command; 7 | use crate::Result; 8 | use std::cmp::{max, min}; 9 | use unicode_width::UnicodeWidthChar; 10 | 11 | // much of the code comes from https://github.com/agatan/termfest/blob/master/src/screen.rs 12 | 13 | /// A Screen is a table of cells to draw on. 14 | /// It's a buffer holding the contents 15 | #[derive(Debug)] 16 | pub struct Screen { 17 | width: usize, 18 | height: usize, 19 | cursor: Cursor, 20 | cells: Vec, 21 | painted_cells: Vec, 22 | painted_cursor: Cursor, 23 | clear_on_start: bool, 24 | } 25 | 26 | impl Screen { 27 | /// create an empty screen with size: (width, height) 28 | pub fn new(width: usize, height: usize) -> Self { 29 | Self { 30 | width, 31 | height, 32 | cells: vec![Cell::default(); width * height], 33 | cursor: Cursor::default(), 34 | painted_cells: vec![Cell::default(); width * height], 35 | painted_cursor: Cursor::default(), 36 | clear_on_start: false, 37 | } 38 | } 39 | 40 | pub fn clear_on_start(&mut self, clear_on_start: bool) { 41 | self.clear_on_start = clear_on_start; 42 | } 43 | 44 | /// get the width of the screen 45 | #[inline] 46 | pub fn width(&self) -> usize { 47 | self.width 48 | } 49 | 50 | /// get the height of the screen 51 | #[inline] 52 | pub fn height(&self) -> usize { 53 | self.height 54 | } 55 | 56 | #[inline] 57 | fn index(&self, row: usize, col: usize) -> Result { 58 | if row >= self.height || col >= self.width { 59 | Err(TuikitError::IndexOutOfBound(row, col)) 60 | } else { 61 | Ok(row * self.width + col) 62 | } 63 | } 64 | 65 | fn empty_canvas(&self, width: usize, height: usize) -> Vec { 66 | vec![Cell::empty(); width * height] 67 | } 68 | 69 | fn copy_cells(&self, original: &[Cell], width: usize, height: usize) -> Vec { 70 | let mut new_cells = self.empty_canvas(width, height); 71 | use std::cmp; 72 | let min_height = cmp::min(height, self.height); 73 | let min_width = cmp::min(width, self.width); 74 | for row in 0..min_height { 75 | let orig_start = row * self.width; 76 | let orig_end = min_width + orig_start; 77 | let start = row * width; 78 | let end = min_width + start; 79 | (&mut new_cells[start..end]).copy_from_slice(&original[orig_start..orig_end]); 80 | } 81 | new_cells 82 | } 83 | 84 | /// to resize the screen to `(width, height)` 85 | pub fn resize(&mut self, width: usize, height: usize) { 86 | self.cells = self.copy_cells(&self.cells, width, height); 87 | self.painted_cells = self.empty_canvas(width, height); 88 | self.width = width; 89 | self.height = height; 90 | 91 | self.cursor.row = min(self.cursor.row, height); 92 | self.cursor.col = min(self.cursor.col, width); 93 | } 94 | 95 | /// sync internal buffer with the terminal 96 | pub fn present(&mut self) -> Vec { 97 | let mut commands = Vec::with_capacity(2048); 98 | let default_attr = Attr::default(); 99 | let mut last_attr = default_attr; 100 | 101 | // hide cursor && reset Attributes 102 | commands.push(Command::CursorShow(false)); 103 | commands.push(Command::CursorGoto { row: 0, col: 0 }); 104 | commands.push(Command::ResetAttributes); 105 | 106 | let mut last_cursor = Cursor::default(); 107 | 108 | for row in 0..self.height { 109 | // calculate the last col that has contents 110 | let mut empty_col_index = 0; 111 | for col in (0..self.width).rev() { 112 | let index = self.index(row, col).unwrap(); 113 | let cell = &self.cells[index]; 114 | if cell.is_empty() { 115 | self.painted_cells[index] = *cell; 116 | } else { 117 | empty_col_index = col + 1; 118 | break; 119 | } 120 | } 121 | 122 | // compare cells and print necessary escape codes 123 | let mut last_ch_is_wide = false; 124 | for col in 0..empty_col_index { 125 | let index = self.index(row, col).unwrap(); 126 | 127 | // advance if the last character is wide 128 | if last_ch_is_wide { 129 | last_ch_is_wide = false; 130 | self.painted_cells[index] = self.cells[index]; 131 | continue; 132 | } 133 | 134 | let cell_to_paint = self.cells[index]; 135 | let cell_painted = self.painted_cells[index]; 136 | 137 | // no need to paint if the content did not change 138 | if cell_to_paint == cell_painted { 139 | continue; 140 | } 141 | 142 | // move cursor if necessary 143 | if last_cursor.row != row || last_cursor.col != col { 144 | commands.push(Command::CursorGoto { row, col }); 145 | } 146 | 147 | if cell_to_paint.attr != last_attr { 148 | commands.push(Command::ResetAttributes); 149 | commands.push(Command::SetAttribute(cell_to_paint.attr)); 150 | last_attr = cell_to_paint.attr; 151 | } 152 | 153 | // correctly draw the characters 154 | match cell_to_paint.ch { 155 | '\n' | '\r' | '\t' | '\0' => { 156 | commands.push(Command::PutChar(' ')); 157 | } 158 | _ => { 159 | commands.push(Command::PutChar(cell_to_paint.ch)); 160 | } 161 | } 162 | 163 | let display_width = cell_to_paint.ch.width().unwrap_or(2); 164 | 165 | // wide character 166 | if display_width == 2 { 167 | last_ch_is_wide = true; 168 | } 169 | 170 | last_cursor.row = row; 171 | last_cursor.col = col + display_width; 172 | self.painted_cells[index] = cell_to_paint; 173 | } 174 | 175 | if empty_col_index != self.width { 176 | commands.push(Command::CursorGoto { 177 | row, 178 | col: empty_col_index, 179 | }); 180 | commands.push(Command::ResetAttributes); 181 | if self.clear_on_start { 182 | commands.push(Command::EraseEndOfLine); 183 | } 184 | last_attr = Attr::default(); 185 | } 186 | } 187 | 188 | // restore cursor 189 | commands.push(Command::CursorGoto { 190 | row: self.cursor.row, 191 | col: self.cursor.col, 192 | }); 193 | if self.cursor.visible { 194 | commands.push(Command::CursorShow(true)); 195 | } 196 | 197 | self.painted_cursor = self.cursor; 198 | 199 | commands 200 | } 201 | 202 | /// ``` 203 | /// use tuikit::cell::Cell; 204 | /// use tuikit::canvas::Canvas; 205 | /// use tuikit::screen::Screen; 206 | /// 207 | /// 208 | /// let mut screen = Screen::new(1, 1); 209 | /// screen.put_cell(0, 0, Cell{ ch: 'a', ..Cell::default()}); 210 | /// let mut iter = screen.iter_cell(); 211 | /// assert_eq!(Some((0, 0, &Cell{ ch: 'a', ..Cell::default()})), iter.next()); 212 | /// assert_eq!(None, iter.next()); 213 | /// ``` 214 | pub fn iter_cell(&self) -> CellIterator { 215 | return CellIterator { 216 | width: self.width, 217 | index: 0, 218 | vec: &self.cells, 219 | }; 220 | } 221 | } 222 | 223 | impl Canvas for Screen { 224 | /// Get the canvas size (width, height) 225 | fn size(&self) -> Result<(usize, usize)> { 226 | Ok((self.width(), self.height())) 227 | } 228 | 229 | /// clear the screen buffer 230 | fn clear(&mut self) -> Result<()> { 231 | for cell in self.cells.iter_mut() { 232 | *cell = Cell::empty(); 233 | } 234 | Ok(()) 235 | } 236 | 237 | /// change a cell of position `(row, col)` to `cell` 238 | fn put_cell(&mut self, row: usize, col: usize, cell: Cell) -> Result { 239 | let ch_width = cell.ch.width().unwrap_or(2); 240 | if ch_width > 1 { 241 | let _ = self.index(row, col + 1).map(|index| { 242 | self.cells[index - 1] = cell; 243 | self.cells[index].ch = ' '; 244 | }); 245 | } else { 246 | let _ = self.index(row, col).map(|index| { 247 | self.cells[index] = cell; 248 | }); 249 | } 250 | Ok(ch_width) 251 | } 252 | 253 | /// move cursor position (row, col) and show cursor 254 | fn set_cursor(&mut self, row: usize, col: usize) -> Result<()> { 255 | self.cursor.row = min(row, max(self.height, 1) - 1); 256 | self.cursor.col = min(col, max(self.width, 1) - 1); 257 | self.cursor.visible = true; 258 | Ok(()) 259 | } 260 | 261 | /// show/hide cursor, set `show` to `false` to hide the cursor 262 | fn show_cursor(&mut self, show: bool) -> Result<()> { 263 | self.cursor.visible = show; 264 | Ok(()) 265 | } 266 | } 267 | 268 | pub struct CellIterator<'a> { 269 | width: usize, 270 | index: usize, 271 | vec: &'a Vec, 272 | } 273 | 274 | impl<'a> Iterator for CellIterator<'a> { 275 | type Item = (usize, usize, &'a Cell); 276 | 277 | fn next(&mut self) -> Option { 278 | if self.index >= self.vec.len() { 279 | return None; 280 | } 281 | 282 | let (row, col) = (self.index / self.width, self.index % self.width); 283 | let ret = self.vec.get(self.index).map(|cell| (row, col, cell)); 284 | self.index += 1; 285 | ret 286 | } 287 | } 288 | 289 | #[derive(Debug, Clone, Copy)] 290 | struct Cursor { 291 | pub row: usize, 292 | pub col: usize, 293 | visible: bool, 294 | } 295 | 296 | impl Default for Cursor { 297 | fn default() -> Self { 298 | Self { 299 | row: 0, 300 | col: 0, 301 | visible: false, 302 | } 303 | } 304 | } 305 | 306 | #[cfg(test)] 307 | mod test { 308 | use super::*; 309 | 310 | #[test] 311 | fn test_cell_iterator() { 312 | let mut screen = Screen::new(2, 2); 313 | let _ = screen.put_cell( 314 | 0, 315 | 0, 316 | Cell { 317 | ch: 'a', 318 | attr: Attr::default(), 319 | }, 320 | ); 321 | let _ = screen.put_cell( 322 | 0, 323 | 1, 324 | Cell { 325 | ch: 'b', 326 | attr: Attr::default(), 327 | }, 328 | ); 329 | let _ = screen.put_cell( 330 | 1, 331 | 0, 332 | Cell { 333 | ch: 'c', 334 | attr: Attr::default(), 335 | }, 336 | ); 337 | let _ = screen.put_cell( 338 | 1, 339 | 1, 340 | Cell { 341 | ch: 'd', 342 | attr: Attr::default(), 343 | }, 344 | ); 345 | 346 | let mut iter = screen.iter_cell(); 347 | assert_eq!( 348 | Some(( 349 | 0, 350 | 0, 351 | &Cell { 352 | ch: 'a', 353 | attr: Attr::default() 354 | } 355 | )), 356 | iter.next() 357 | ); 358 | assert_eq!( 359 | Some(( 360 | 0, 361 | 1, 362 | &Cell { 363 | ch: 'b', 364 | attr: Attr::default() 365 | } 366 | )), 367 | iter.next() 368 | ); 369 | assert_eq!( 370 | Some(( 371 | 1, 372 | 0, 373 | &Cell { 374 | ch: 'c', 375 | attr: Attr::default() 376 | } 377 | )), 378 | iter.next() 379 | ); 380 | assert_eq!( 381 | Some(( 382 | 1, 383 | 1, 384 | &Cell { 385 | ch: 'd', 386 | attr: Attr::default() 387 | } 388 | )), 389 | iter.next() 390 | ); 391 | assert_eq!(None, iter.next()); 392 | 393 | let empty_screen = Screen::new(0, 0); 394 | let mut empty_iter = empty_screen.iter_cell(); 395 | assert_eq!(None, empty_iter.next()); 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /src/output.rs: -------------------------------------------------------------------------------- 1 | //! `Output` is the output stream that deals with ANSI Escape codes. 2 | //! normally you should not use it directly. 3 | //! 4 | //! ``` 5 | //! use std::io; 6 | //! use tuikit::attr::Color; 7 | //! use tuikit::output::Output; 8 | //! 9 | //! let mut output = Output::new(Box::new(io::stdout())).unwrap(); 10 | //! output.set_fg(Color::YELLOW); 11 | //! output.write("YELLOW\n"); 12 | //! output.flush(); 13 | //! 14 | //! ``` 15 | 16 | use std::io; 17 | use std::io::Write; 18 | use std::os::unix::io::AsRawFd; 19 | 20 | use crate::attr::{Attr, Color, Effect}; 21 | use crate::sys::size::terminal_size; 22 | 23 | use term::terminfo::parm::{expand, Param, Variables}; 24 | use term::terminfo::TermInfo; 25 | 26 | // modeled after python-prompt-toolkit 27 | // term info: https://ftp.netbsd.org/pub/NetBSD/NetBSD-release-7/src/share/terminfo/terminfo 28 | 29 | const DEFAULT_BUFFER_SIZE: usize = 1024; 30 | 31 | /// Output is an abstraction over the ANSI codes. 32 | pub struct Output { 33 | /// A callable which returns the `Size` of the output terminal. 34 | buffer: Vec, 35 | stdout: Box, 36 | /// The terminal environment variable. (xterm, xterm-256color, linux, ...) 37 | terminfo: TermInfo, 38 | } 39 | 40 | pub trait WriteAndAsRawFdAndSend: Write + AsRawFd + Send {} 41 | 42 | impl WriteAndAsRawFdAndSend for T where T: Write + AsRawFd + Send {} 43 | 44 | impl Output { 45 | pub fn new(stdout: Box) -> io::Result { 46 | Result::Ok(Self { 47 | buffer: Vec::with_capacity(DEFAULT_BUFFER_SIZE), 48 | stdout, 49 | terminfo: TermInfo::from_env()?, 50 | }) 51 | } 52 | 53 | fn write_cap(&mut self, cmd: &str) { 54 | self.write_cap_with_params(cmd, &[]) 55 | } 56 | 57 | fn write_cap_with_params(&mut self, cap: &str, params: &[Param]) { 58 | if let Some(cmd) = self.terminfo.strings.get(cap) { 59 | if let Ok(s) = expand(cmd, params, &mut Variables::new()) { 60 | self.buffer.extend(&s); 61 | } 62 | } 63 | } 64 | 65 | /// Write text (Terminal escape sequences will be removed/escaped.) 66 | pub fn write(&mut self, data: &str) { 67 | self.buffer.extend(data.replace("\x1b", "?").as_bytes()); 68 | } 69 | 70 | /// Write raw texts to the terminal. 71 | pub fn write_raw(&mut self, data: &[u8]) { 72 | self.buffer.extend_from_slice(data); 73 | } 74 | 75 | /// Return the encoding for this output, e.g. 'utf-8'. 76 | /// (This is used mainly to know which characters are supported by the 77 | /// output the data, so that the UI can provide alternatives, when 78 | /// required.) 79 | pub fn encoding(&self) -> &str { 80 | unimplemented!() 81 | } 82 | 83 | /// Set terminal title. 84 | pub fn set_title(&mut self, title: &str) { 85 | if self.terminfo.names.contains(&"linux".to_string()) 86 | || self.terminfo.names.contains(&"eterm-color".to_string()) 87 | { 88 | return; 89 | } 90 | 91 | let title = title.replace("\x1b", "").replace("\x07", ""); 92 | self.write_raw(format!("\x1b]2;{}\x07", title).as_bytes()); 93 | } 94 | 95 | /// Clear title again. (or restore previous title.) 96 | pub fn clear_title(&mut self) { 97 | self.set_title(""); 98 | } 99 | 100 | /// Write to output stream and flush. 101 | pub fn flush(&mut self) { 102 | let _ = self.stdout.write(&self.buffer); 103 | self.buffer.clear(); 104 | let _ = self.stdout.flush(); 105 | } 106 | 107 | /// Erases the screen with the background colour and moves the cursor to home. 108 | pub fn erase_screen(&mut self) { 109 | self.write_cap("clear"); 110 | } 111 | 112 | /// Go to the alternate screen buffer. (For full screen applications). 113 | pub fn enter_alternate_screen(&mut self) { 114 | self.write_cap("smcup"); 115 | } 116 | 117 | /// Leave the alternate screen buffer. 118 | pub fn quit_alternate_screen(&mut self) { 119 | self.write_cap("rmcup"); 120 | } 121 | 122 | /// Enable mouse. 123 | pub fn enable_mouse_support(&mut self) { 124 | self.write_raw("\x1b[?1000h".as_bytes()); 125 | 126 | // Enable urxvt Mouse mode. (For terminals that understand this.) 127 | self.write_raw("\x1b[?1015h".as_bytes()); 128 | 129 | // Also enable Xterm SGR mouse mode. (For terminals that understand this.) 130 | self.write_raw("\x1b[?1006h".as_bytes()); 131 | 132 | // Note: E.g. lxterminal understands 1000h, but not the urxvt or sgr extensions. 133 | } 134 | 135 | /// Disable mouse. 136 | pub fn disable_mouse_support(&mut self) { 137 | self.write_raw("\x1b[?1000l".as_bytes()); 138 | self.write_raw("\x1b[?1015l".as_bytes()); 139 | self.write_raw("\x1b[?1006l".as_bytes()); 140 | } 141 | 142 | /// Erases from the current cursor position to the end of the current line. 143 | pub fn erase_end_of_line(&mut self) { 144 | self.write_cap("el"); 145 | } 146 | 147 | /// Erases the screen from the current line down to the bottom of the screen. 148 | pub fn erase_down(&mut self) { 149 | self.write_cap("ed"); 150 | } 151 | 152 | /// Reset color and styling attributes. 153 | pub fn reset_attributes(&mut self) { 154 | self.write_cap("sgr0"); 155 | } 156 | 157 | /// Set current foreground color 158 | pub fn set_fg(&mut self, color: Color) { 159 | match color { 160 | Color::Default => { 161 | self.write_raw("\x1b[39m".as_bytes()); 162 | } 163 | Color::AnsiValue(x) => { 164 | self.write_cap_with_params("setaf", &[Param::Number(x as i32)]); 165 | } 166 | Color::Rgb(r, g, b) => { 167 | self.write_raw(format!("\x1b[38;2;{};{};{}m", r, g, b).as_bytes()); 168 | } 169 | Color::__Nonexhaustive => unreachable!(), 170 | } 171 | } 172 | 173 | /// Set current background color 174 | pub fn set_bg(&mut self, color: Color) { 175 | match color { 176 | Color::Default => { 177 | self.write_raw("\x1b[49m".as_bytes()); 178 | } 179 | Color::AnsiValue(x) => { 180 | self.write_cap_with_params("setab", &[Param::Number(x as i32)]); 181 | } 182 | Color::Rgb(r, g, b) => { 183 | self.write_raw(format!("\x1b[48;2;{};{};{}m", r, g, b).as_bytes()); 184 | } 185 | Color::__Nonexhaustive => unreachable!(), 186 | } 187 | } 188 | 189 | /// Set current effect (underline, bold, etc) 190 | pub fn set_effect(&mut self, effect: Effect) { 191 | if effect.contains(Effect::BOLD) { 192 | self.write_cap("bold"); 193 | } 194 | if effect.contains(Effect::DIM) { 195 | self.write_cap("dim"); 196 | } 197 | if effect.contains(Effect::UNDERLINE) { 198 | self.write_cap("smul"); 199 | } 200 | if effect.contains(Effect::BLINK) { 201 | self.write_cap("blink"); 202 | } 203 | if effect.contains(Effect::REVERSE) { 204 | self.write_cap("rev"); 205 | } 206 | } 207 | 208 | /// Set new color and styling attributes. 209 | pub fn set_attribute(&mut self, attr: Attr) { 210 | self.set_fg(attr.fg); 211 | self.set_bg(attr.bg); 212 | self.set_effect(attr.effect); 213 | } 214 | 215 | /// Disable auto line wrapping. 216 | pub fn disable_autowrap(&mut self) { 217 | self.write_cap("rmam"); 218 | } 219 | 220 | /// Enable auto line wrapping. 221 | pub fn enable_autowrap(&mut self) { 222 | self.write_cap("smam"); 223 | } 224 | 225 | /// Move cursor position. 226 | pub fn cursor_goto(&mut self, row: usize, column: usize) { 227 | self.write_cap_with_params( 228 | "cup", 229 | &[Param::Number(row as i32), Param::Number(column as i32)], 230 | ); 231 | } 232 | 233 | /// Move cursor `amount` place up. 234 | pub fn cursor_up(&mut self, amount: usize) { 235 | match amount { 236 | 0 => {} 237 | 1 => self.write_cap("cuu1"), 238 | _ => self.write_cap_with_params("cuu", &[Param::Number(amount as i32)]), 239 | } 240 | } 241 | 242 | /// Move cursor `amount` place down. 243 | pub fn cursor_down(&mut self, amount: usize) { 244 | match amount { 245 | 0 => {} 246 | 1 => self.write_cap("cud1"), 247 | _ => self.write_cap_with_params("cud", &[Param::Number(amount as i32)]), 248 | } 249 | } 250 | 251 | /// Move cursor `amount` place forward. 252 | pub fn cursor_forward(&mut self, amount: usize) { 253 | match amount { 254 | 0 => {} 255 | 1 => self.write_cap("cuf1"), 256 | _ => self.write_cap_with_params("cuf", &[Param::Number(amount as i32)]), 257 | } 258 | } 259 | 260 | /// Move cursor `amount` place backward. 261 | pub fn cursor_backward(&mut self, amount: usize) { 262 | match amount { 263 | 0 => {} 264 | 1 => self.write_cap("cub1"), 265 | _ => self.write_cap_with_params("cub", &[Param::Number(amount as i32)]), 266 | } 267 | } 268 | 269 | /// Hide cursor. 270 | pub fn hide_cursor(&mut self) { 271 | self.write_cap("civis"); 272 | } 273 | 274 | /// Show cursor. 275 | pub fn show_cursor(&mut self) { 276 | self.write_cap("cnorm"); 277 | } 278 | 279 | /// Asks for a cursor position report (CPR). (VT100 only.) 280 | pub fn ask_for_cpr(&mut self) { 281 | self.write_raw("\x1b[6n".as_bytes()); 282 | self.flush() 283 | } 284 | 285 | /// Sound bell. 286 | pub fn bell(&mut self) { 287 | self.write_cap("bel"); 288 | self.flush() 289 | } 290 | 291 | /// get terminal size (width, height) 292 | pub fn terminal_size(&self) -> io::Result<(usize, usize)> { 293 | terminal_size(self.stdout.as_raw_fd()) 294 | } 295 | 296 | /// For vt100/xterm etc. 297 | pub fn enable_bracketed_paste(&mut self) { 298 | self.write_raw("\x1b[?2004h".as_bytes()); 299 | } 300 | 301 | /// For vt100/xterm etc. 302 | pub fn disable_bracketed_paste(&mut self) { 303 | self.write_raw("\x1b[?2004l".as_bytes()); 304 | } 305 | 306 | /// Execute the command 307 | pub fn execute(&mut self, cmd: Command) { 308 | match cmd { 309 | Command::PutChar(c) => self.write(c.to_string().as_str()), 310 | Command::Write(content) => self.write(&content), 311 | Command::SetTitle(title) => self.set_title(&title), 312 | Command::ClearTitle => self.clear_title(), 313 | Command::Flush => self.flush(), 314 | Command::EraseScreen => self.erase_screen(), 315 | Command::AlternateScreen(enable) => { 316 | if enable { 317 | self.enter_alternate_screen() 318 | } else { 319 | self.quit_alternate_screen() 320 | } 321 | } 322 | Command::MouseSupport(enable) => { 323 | if enable { 324 | self.enable_mouse_support(); 325 | } else { 326 | self.disable_mouse_support(); 327 | } 328 | } 329 | Command::EraseEndOfLine => self.erase_end_of_line(), 330 | Command::EraseDown => self.erase_down(), 331 | Command::ResetAttributes => self.reset_attributes(), 332 | Command::Fg(fg) => self.set_fg(fg), 333 | Command::Bg(bg) => self.set_bg(bg), 334 | Command::Effect(effect) => self.set_effect(effect), 335 | Command::SetAttribute(attr) => self.set_attribute(attr), 336 | Command::AutoWrap(enable) => { 337 | if enable { 338 | self.enable_autowrap(); 339 | } else { 340 | self.disable_autowrap(); 341 | } 342 | } 343 | Command::CursorGoto { row, col } => self.cursor_goto(row, col), 344 | Command::CursorUp(amount) => self.cursor_up(amount), 345 | Command::CursorDown(amount) => self.cursor_down(amount), 346 | Command::CursorLeft(amount) => self.cursor_backward(amount), 347 | Command::CursorRight(amount) => self.cursor_forward(amount), 348 | Command::CursorShow(show) => { 349 | if show { 350 | self.show_cursor() 351 | } else { 352 | self.hide_cursor() 353 | } 354 | } 355 | Command::BracketedPaste(enable) => { 356 | if enable { 357 | self.enable_bracketed_paste() 358 | } else { 359 | self.disable_bracketed_paste() 360 | } 361 | } 362 | } 363 | } 364 | } 365 | 366 | /// Instead of calling functions of `Output`, we could send commands. 367 | #[derive(Debug, Clone)] 368 | pub enum Command { 369 | /// Put a char to screen 370 | PutChar(char), 371 | /// Write content to screen (escape codes will be escaped) 372 | Write(String), 373 | /// Set the title of the terminal 374 | SetTitle(String), 375 | /// Clear the title of the terminal 376 | ClearTitle, 377 | /// Flush all the buffered contents 378 | Flush, 379 | /// Erase the entire screen 380 | EraseScreen, 381 | /// Enter(true)/Quit(false) the alternate screen mode 382 | AlternateScreen(bool), 383 | /// Enable(true)/Disable(false) mouse support 384 | MouseSupport(bool), 385 | /// Erase contents to the end of current line 386 | EraseEndOfLine, 387 | /// Erase contents till the bottom of the screen 388 | EraseDown, 389 | /// Reset attributes 390 | ResetAttributes, 391 | /// Set the foreground color 392 | Fg(Color), 393 | /// Set the background color 394 | Bg(Color), 395 | /// Set the effect(e.g. underline, dim, bold, ...) 396 | Effect(Effect), 397 | /// Set the fg, bg & effect. 398 | SetAttribute(Attr), 399 | /// Enable(true)/Disable(false) autowrap 400 | AutoWrap(bool), 401 | /// move the cursor to `(row, col)` 402 | CursorGoto { row: usize, col: usize }, 403 | /// move cursor up `x` lines 404 | CursorUp(usize), 405 | /// move cursor down `x` lines 406 | CursorDown(usize), 407 | /// move cursor left `x` characters 408 | CursorLeft(usize), 409 | /// move cursor right `x` characters 410 | CursorRight(usize), 411 | /// Show(true)/Hide(false) cursor 412 | CursorShow(bool), 413 | /// Enable(true)/Disable(false) the bracketed paste mode 414 | BracketedPaste(bool), 415 | } 416 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | //! module to handle keystrokes 2 | //! 3 | //! ```no_run 4 | //! use tuikit::input::KeyBoard; 5 | //! use tuikit::key::Key; 6 | //! use std::time::Duration; 7 | //! let mut keyboard = KeyBoard::new_with_tty(); 8 | //! let key = keyboard.next_key(); 9 | //! ``` 10 | 11 | use std::fs::File; 12 | use std::io::prelude::*; 13 | use std::os::unix::io::AsRawFd; 14 | use std::os::unix::io::FromRawFd; 15 | use std::sync::Arc; 16 | use std::time::{Duration, Instant}; 17 | 18 | use nix::fcntl::{fcntl, FcntlArg, OFlag}; 19 | 20 | use crate::error::TuikitError; 21 | use crate::key::Key::*; 22 | use crate::key::{Key, MouseButton}; 23 | use crate::raw::get_tty; 24 | use crate::spinlock::SpinLock; 25 | use crate::sys::file::wait_until_ready; 26 | use crate::Result; 27 | 28 | pub trait ReadAndAsRawFd: Read + AsRawFd + Send {} 29 | 30 | const KEY_WAIT: Duration = Duration::from_millis(10); 31 | const DOUBLE_CLICK_DURATION: u128 = 300; 32 | 33 | impl ReadAndAsRawFd for T where T: Read + AsRawFd + Send {} 34 | 35 | pub struct KeyBoard { 36 | file: Box, 37 | sig_tx: Arc>, 38 | sig_rx: File, 39 | // bytes will be poped from front, normally the buffer size will be small(< 10 bytes) 40 | byte_buf: Vec, 41 | 42 | raw_mouse: bool, 43 | next_key: Option>, 44 | last_click: Key, 45 | last_click_time: SpinLock, 46 | } 47 | 48 | // https://www.xfree86.org/4.8.0/ctlseqs.html 49 | // http://man7.org/linux/man-pages/man4/console_codes.4.html 50 | impl KeyBoard { 51 | pub fn new(file: Box) -> Self { 52 | // the self-pipe trick for interrupt `select` 53 | let (rx, tx) = nix::unistd::pipe().expect("failed to set pipe"); 54 | 55 | // set the signal pipe to non-blocking mode 56 | let flag = fcntl(rx, FcntlArg::F_GETFL).expect("Get fcntl failed"); 57 | let mut flag = OFlag::from_bits_truncate(flag); 58 | flag.insert(OFlag::O_NONBLOCK); 59 | let _ = fcntl(rx, FcntlArg::F_SETFL(flag)); 60 | 61 | // set file to non-blocking mode 62 | let flag = fcntl(file.as_raw_fd(), FcntlArg::F_GETFL).expect("Get fcntl failed"); 63 | let mut flag = OFlag::from_bits_truncate(flag); 64 | flag.insert(OFlag::O_NONBLOCK); 65 | let _ = fcntl(file.as_raw_fd(), FcntlArg::F_SETFL(flag)); 66 | 67 | KeyBoard { 68 | file, 69 | sig_tx: Arc::new(SpinLock::new(unsafe { File::from_raw_fd(tx) })), 70 | sig_rx: unsafe { File::from_raw_fd(rx) }, 71 | byte_buf: Vec::new(), 72 | raw_mouse: false, 73 | next_key: None, 74 | last_click: Key::Null, 75 | last_click_time: SpinLock::new(Instant::now()), 76 | } 77 | } 78 | 79 | pub fn new_with_tty() -> Self { 80 | Self::new(Box::new( 81 | get_tty().expect("KeyBoard::new_with_tty: failed to get tty"), 82 | )) 83 | } 84 | 85 | pub fn raw_mouse(mut self, raw_mouse: bool) -> Self { 86 | self.raw_mouse = raw_mouse; 87 | self 88 | } 89 | 90 | pub fn get_interrupt_handler(&self) -> KeyboardHandler { 91 | KeyboardHandler { 92 | handler: self.sig_tx.clone(), 93 | } 94 | } 95 | 96 | fn fetch_bytes(&mut self, timeout: Duration) -> Result<()> { 97 | let mut reader_buf = [0; 1]; 98 | 99 | // clear interrupt signal 100 | while let Ok(_) = self.sig_rx.read(&mut reader_buf) {} 101 | 102 | wait_until_ready( 103 | self.file.as_raw_fd(), 104 | Some(self.sig_rx.as_raw_fd()), 105 | timeout, 106 | )?; // wait timeout 107 | 108 | self.read_unread_bytes(); 109 | Ok(()) 110 | } 111 | 112 | fn read_unread_bytes(&mut self) { 113 | let mut reader_buf = [0; 1]; 114 | while let Ok(_) = self.file.read(&mut reader_buf) { 115 | self.byte_buf.push(reader_buf[0]); 116 | } 117 | } 118 | 119 | #[allow(dead_code)] 120 | fn next_byte(&mut self) -> Result { 121 | self.next_byte_timeout(Duration::new(0, 0)) 122 | } 123 | 124 | fn next_byte_timeout(&mut self, timeout: Duration) -> Result { 125 | trace!("next_byte_timeout: timeout: {:?}", timeout); 126 | if self.byte_buf.is_empty() { 127 | self.fetch_bytes(timeout)?; 128 | } 129 | 130 | trace!("next_byte_timeout: after fetch, buf = {:?}", self.byte_buf); 131 | Ok(self.byte_buf.remove(0)) 132 | } 133 | 134 | #[allow(dead_code)] 135 | fn next_char(&mut self) -> Result { 136 | self.next_char_timeout(Duration::new(0, 0)) 137 | } 138 | 139 | fn next_char_timeout(&mut self, timeout: Duration) -> Result { 140 | trace!("next_char_timeout: timeout: {:?}", timeout); 141 | if self.byte_buf.is_empty() { 142 | self.fetch_bytes(timeout)?; 143 | } 144 | 145 | trace!("get_chars: buf: {:?}", self.byte_buf); 146 | let bytes = std::mem::replace(&mut self.byte_buf, Vec::new()); 147 | match String::from_utf8(bytes) { 148 | Ok(string) => { 149 | let ret = string 150 | .chars() 151 | .next() 152 | .expect("failed to get next char from input"); 153 | self.byte_buf 154 | .extend_from_slice(&string.as_bytes()[ret.len_utf8()..]); 155 | Ok(ret) 156 | } 157 | Err(error) => { 158 | let valid_up_to = error.utf8_error().valid_up_to(); 159 | let bytes = error.into_bytes(); 160 | let string = String::from_utf8_lossy(&bytes[..valid_up_to]); 161 | let ret = string 162 | .chars() 163 | .next() 164 | .expect("failed to get next char from input"); 165 | self.byte_buf.extend_from_slice(&bytes[ret.len_utf8()..]); 166 | Ok(ret) 167 | } 168 | } 169 | } 170 | 171 | fn merge_wheel(&mut self, current_key: Result) -> (Result, Option>) { 172 | match current_key { 173 | Ok(Key::MousePress(key @ MouseButton::WheelUp, row, col)) 174 | | Ok(Key::MousePress(key @ MouseButton::WheelDown, row, col)) => { 175 | let mut count = 1; 176 | let mut o_next_key; 177 | loop { 178 | o_next_key = self.try_next_raw_key(); 179 | match o_next_key { 180 | Some(Ok(Key::MousePress(k, r, c))) if key == k && row == r && col == c => { 181 | count += 1 182 | } 183 | _ => break, 184 | } 185 | } 186 | 187 | match key { 188 | MouseButton::WheelUp => (Ok(Key::WheelUp(row, col, count)), o_next_key), 189 | MouseButton::WheelDown => (Ok(Key::WheelDown(row, col, count)), o_next_key), 190 | _ => unreachable!(), 191 | } 192 | } 193 | _ => (current_key, None), 194 | } 195 | } 196 | 197 | pub fn next_key(&mut self) -> Result { 198 | self.next_key_timeout(Duration::new(0, 0)) 199 | } 200 | 201 | pub fn next_key_timeout(&mut self, timeout: Duration) -> Result { 202 | if self.raw_mouse { 203 | return self.next_raw_key_timeout(timeout); 204 | } 205 | 206 | let next_key = if self.next_key.is_some() { 207 | self.next_key.take().unwrap() 208 | } else { 209 | // fetch next key 210 | let next_key = self.next_raw_key_timeout(timeout); 211 | let (next_key, next_next_key) = self.merge_wheel(next_key); 212 | self.next_key = next_next_key; 213 | next_key 214 | }; 215 | 216 | // parse double click 217 | match next_key { 218 | Ok(key @ MousePress(..)) => { 219 | if let MousePress(button, row, col) = key { 220 | let ret = if key == self.last_click 221 | && self.last_click_time.lock().elapsed().as_millis() < DOUBLE_CLICK_DURATION 222 | { 223 | DoubleClick(button, row, col) 224 | } else { 225 | self.last_click = key; 226 | SingleClick(button, row, col) 227 | }; 228 | 229 | *self.last_click_time.lock() = Instant::now(); 230 | Ok(ret) 231 | } else { 232 | unreachable!(); 233 | } 234 | } 235 | _ => return next_key, 236 | } 237 | } 238 | 239 | #[allow(dead_code)] 240 | fn next_raw_key(&mut self) -> Result { 241 | self.next_raw_key_timeout(Duration::new(0, 0)) 242 | } 243 | 244 | fn try_next_raw_key(&mut self) -> Option> { 245 | match self.next_raw_key_timeout(KEY_WAIT) { 246 | Ok(key) => Some(Ok(key)), 247 | Err(TuikitError::Timeout(_)) => None, 248 | Err(error) => Some(Err(error)), 249 | } 250 | } 251 | 252 | /// Wait `timeout` until next key stroke 253 | fn next_raw_key_timeout(&mut self, timeout: Duration) -> Result { 254 | trace!("next_raw_key_timeout: {:?}", timeout); 255 | let ch = self.next_char_timeout(timeout)?; 256 | match ch { 257 | '\u{00}' => Ok(Ctrl(' ')), 258 | '\u{01}' => Ok(Ctrl('a')), 259 | '\u{02}' => Ok(Ctrl('b')), 260 | '\u{03}' => Ok(Ctrl('c')), 261 | '\u{04}' => Ok(Ctrl('d')), 262 | '\u{05}' => Ok(Ctrl('e')), 263 | '\u{06}' => Ok(Ctrl('f')), 264 | '\u{07}' => Ok(Ctrl('g')), 265 | '\u{08}' => Ok(Ctrl('h')), 266 | '\u{09}' => Ok(Tab), 267 | '\u{0A}' => Ok(Ctrl('j')), 268 | '\u{0B}' => Ok(Ctrl('k')), 269 | '\u{0C}' => Ok(Ctrl('l')), 270 | '\u{0D}' => Ok(Enter), 271 | '\u{0E}' => Ok(Ctrl('n')), 272 | '\u{0F}' => Ok(Ctrl('o')), 273 | '\u{10}' => Ok(Ctrl('p')), 274 | '\u{11}' => Ok(Ctrl('q')), 275 | '\u{12}' => Ok(Ctrl('r')), 276 | '\u{13}' => Ok(Ctrl('s')), 277 | '\u{14}' => Ok(Ctrl('t')), 278 | '\u{15}' => Ok(Ctrl('u')), 279 | '\u{16}' => Ok(Ctrl('v')), 280 | '\u{17}' => Ok(Ctrl('w')), 281 | '\u{18}' => Ok(Ctrl('x')), 282 | '\u{19}' => Ok(Ctrl('y')), 283 | '\u{1A}' => Ok(Ctrl('z')), 284 | '\u{1B}' => self.escape_sequence(), 285 | '\u{7F}' => Ok(Backspace), 286 | ch => Ok(Char(ch)), 287 | } 288 | } 289 | 290 | fn escape_sequence(&mut self) -> Result { 291 | let seq1 = self.next_char_timeout(KEY_WAIT).unwrap_or('\u{1B}'); 292 | match seq1 { 293 | '[' => self.escape_csi(), 294 | 'O' => self.escape_o(), 295 | _ => self.parse_alt(seq1), 296 | } 297 | } 298 | 299 | fn parse_alt(&mut self, ch: char) -> Result { 300 | match ch { 301 | '\u{1B}' => { 302 | match self.next_byte_timeout(KEY_WAIT) { 303 | Ok(b'[') => {} 304 | Ok(c) => { 305 | return Err(TuikitError::UnknownSequence(format!("ESC ESC {}", c))); 306 | } 307 | Err(_) => return Ok(ESC), 308 | } 309 | 310 | match self.escape_csi() { 311 | Ok(Up) => Ok(AltUp), 312 | Ok(Down) => Ok(AltDown), 313 | Ok(Left) => Ok(AltLeft), 314 | Ok(Right) => Ok(AltRight), 315 | Ok(PageUp) => Ok(AltPageUp), 316 | Ok(PageDown) => Ok(AltPageDown), 317 | _ => Err(TuikitError::UnknownSequence(format!("ESC ESC [ ..."))), 318 | } 319 | } 320 | '\u{00}' => Ok(CtrlAlt(' ')), 321 | '\u{01}' => Ok(CtrlAlt('a')), 322 | '\u{02}' => Ok(CtrlAlt('b')), 323 | '\u{03}' => Ok(CtrlAlt('c')), 324 | '\u{04}' => Ok(CtrlAlt('d')), 325 | '\u{05}' => Ok(CtrlAlt('e')), 326 | '\u{06}' => Ok(CtrlAlt('f')), 327 | '\u{07}' => Ok(CtrlAlt('g')), 328 | '\u{08}' => Ok(CtrlAlt('h')), 329 | '\u{09}' => Ok(AltTab), 330 | '\u{0A}' => Ok(CtrlAlt('j')), 331 | '\u{0B}' => Ok(CtrlAlt('k')), 332 | '\u{0C}' => Ok(CtrlAlt('l')), 333 | '\u{0D}' => Ok(AltEnter), 334 | '\u{0E}' => Ok(CtrlAlt('n')), 335 | '\u{0F}' => Ok(CtrlAlt('o')), 336 | '\u{10}' => Ok(CtrlAlt('p')), 337 | '\u{11}' => Ok(CtrlAlt('q')), 338 | '\u{12}' => Ok(CtrlAlt('r')), 339 | '\u{13}' => Ok(CtrlAlt('s')), 340 | '\u{14}' => Ok(CtrlAlt('t')), 341 | '\u{15}' => Ok(CtrlAlt('u')), 342 | '\u{16}' => Ok(CtrlAlt('v')), 343 | '\u{17}' => Ok(CtrlAlt('w')), 344 | '\u{18}' => Ok(CtrlAlt('x')), 345 | '\u{19}' => Ok(AltBackTab), 346 | '\u{1A}' => Ok(CtrlAlt('z')), 347 | '\u{7F}' => Ok(AltBackspace), 348 | ch => Ok(Alt(ch)), 349 | } 350 | } 351 | 352 | fn escape_csi(&mut self) -> Result { 353 | let cursor_pos = self.parse_cursor_report(); 354 | if cursor_pos.is_ok() { 355 | return cursor_pos; 356 | } 357 | 358 | let seq2 = self.next_byte_timeout(KEY_WAIT)?; 359 | match seq2 { 360 | b'0' | b'9' => Err(TuikitError::UnknownSequence(format!("ESC [ {:x?}", seq2))), 361 | b'1'..=b'8' => self.extended_escape(seq2), 362 | b'[' => { 363 | // Linux Console ESC [ [ _ 364 | let seq3 = self.next_byte_timeout(KEY_WAIT)?; 365 | match seq3 { 366 | b'A' => Ok(F(1)), 367 | b'B' => Ok(F(2)), 368 | b'C' => Ok(F(3)), 369 | b'D' => Ok(F(4)), 370 | b'E' => Ok(F(5)), 371 | _ => Err(TuikitError::UnknownSequence(format!("ESC [ [ {:x?}", seq3))), 372 | } 373 | } 374 | b'A' => Ok(Up), // kcuu1 375 | b'B' => Ok(Down), // kcud1 376 | b'C' => Ok(Right), // kcuf1 377 | b'D' => Ok(Left), // kcub1 378 | b'H' => Ok(Home), // khome 379 | b'F' => Ok(End), 380 | b'Z' => Ok(BackTab), 381 | b'M' => { 382 | // X10 emulation mouse encoding: ESC [ M Bxy (6 characters only) 383 | let cb = self.next_byte_timeout(KEY_WAIT)?; 384 | // (1, 1) are the coords for upper left. 385 | let cx = self.next_byte_timeout(KEY_WAIT)?.saturating_sub(32) as u16 - 1; // 0 based 386 | let cy = self.next_byte_timeout(KEY_WAIT)?.saturating_sub(32) as u16 - 1; // 0 based 387 | match cb & 0b11 { 388 | 0 => { 389 | if cb & 0x40 != 0 { 390 | Ok(MousePress(MouseButton::WheelUp, cy, cx)) 391 | } else { 392 | Ok(MousePress(MouseButton::Left, cy, cx)) 393 | } 394 | } 395 | 1 => { 396 | if cb & 0x40 != 0 { 397 | Ok(MousePress(MouseButton::WheelDown, cy, cx)) 398 | } else { 399 | Ok(MousePress(MouseButton::Middle, cy, cx)) 400 | } 401 | } 402 | 2 => Ok(MousePress(MouseButton::Right, cy, cx)), 403 | 3 => Ok(MouseRelease(cy, cx)), 404 | _ => Err(TuikitError::UnknownSequence(format!( 405 | "ESC M {:?}{:?}{:?}", 406 | cb, cx, cy 407 | ))), 408 | } 409 | } 410 | b'<' => { 411 | // xterm mouse encoding: 412 | // ESC [ < Cb ; Cx ; Cy ; (M or m) 413 | self.read_unread_bytes(); 414 | if !self.byte_buf.contains(&b'm') && !self.byte_buf.contains(&b'M') { 415 | return Err(TuikitError::UnknownSequence(format!( 416 | "ESC [ < (not ending with m/M)" 417 | ))); 418 | } 419 | 420 | let mut str_buf = String::new(); 421 | let mut c = self.next_char_timeout(KEY_WAIT)?; 422 | while c != 'm' && c != 'M' { 423 | str_buf.push(c); 424 | c = self.next_char_timeout(KEY_WAIT)?; 425 | } 426 | let nums = &mut str_buf.split(';'); 427 | 428 | let cb = nums.next().unwrap().parse::().unwrap(); 429 | let cx = nums.next().unwrap().parse::().unwrap() - 1; // 0 based 430 | let cy = nums.next().unwrap().parse::().unwrap() - 1; // 0 based 431 | 432 | match cb { 433 | 0..=2 | 64..=65 => { 434 | let button = match cb { 435 | 0 => MouseButton::Left, 436 | 1 => MouseButton::Middle, 437 | 2 => MouseButton::Right, 438 | 64 => MouseButton::WheelUp, 439 | 65 => MouseButton::WheelDown, 440 | _ => { 441 | return Err(TuikitError::UnknownSequence(format!( 442 | "ESC [ < {} {}", 443 | str_buf, c 444 | ))); 445 | } 446 | }; 447 | 448 | match c { 449 | 'M' => Ok(MousePress(button, cy, cx)), 450 | 'm' => Ok(MouseRelease(cy, cx)), 451 | _ => Err(TuikitError::UnknownSequence(format!( 452 | "ESC [ < {} {}", 453 | str_buf, c 454 | ))), 455 | } 456 | } 457 | 32 => Ok(MouseHold(cy, cx)), 458 | _ => Err(TuikitError::UnknownSequence(format!( 459 | "ESC [ < {} {}", 460 | str_buf, c 461 | ))), 462 | } 463 | } 464 | _ => Err(TuikitError::UnknownSequence(format!("ESC [ {:?}", seq2))), 465 | } 466 | } 467 | 468 | fn parse_cursor_report(&mut self) -> Result { 469 | self.read_unread_bytes(); 470 | let pos_semi = self.byte_buf.iter().position(|&b| b == b';'); 471 | let pos_r = self.byte_buf.iter().position(|&b| b == b'R'); 472 | 473 | if pos_semi.is_some() && pos_r.is_some() { 474 | let pos_semi = pos_semi.unwrap(); 475 | let pos_r = pos_r.unwrap(); 476 | 477 | let remain = self.byte_buf.split_off(pos_r + 1); 478 | let mut col_str = self.byte_buf.split_off(pos_semi + 1); 479 | let mut row_str = std::mem::replace(&mut self.byte_buf, remain); 480 | 481 | row_str.pop(); // remove the ';' character 482 | col_str.pop(); // remove the 'R' character 483 | let row = String::from_utf8(row_str)?; 484 | let col = String::from_utf8(col_str)?; 485 | 486 | let row_num = row.parse::()?; 487 | let col_num = col.parse::()?; 488 | Ok(CursorPos(row_num - 1, col_num - 1)) 489 | } else { 490 | Err(TuikitError::NoCursorReportResponse) 491 | } 492 | } 493 | 494 | fn extended_escape(&mut self, seq2: u8) -> Result { 495 | let seq3 = self.next_byte_timeout(KEY_WAIT)?; 496 | if seq3 == b'~' { 497 | match seq2 { 498 | b'1' | b'7' => Ok(Home), // tmux, xrvt 499 | b'2' => Ok(Insert), 500 | b'3' => Ok(Delete), // kdch1 501 | b'4' | b'8' => Ok(End), // tmux, xrvt 502 | b'5' => Ok(PageUp), // kpp 503 | b'6' => Ok(PageDown), // knp 504 | _ => Err(TuikitError::UnknownSequence(format!("ESC [ {} ~", seq2))), 505 | } 506 | } else if seq3 >= b'0' && seq3 <= b'9' { 507 | let mut str_buf = String::new(); 508 | str_buf.push(seq2 as char); 509 | str_buf.push(seq3 as char); 510 | 511 | let mut seq_last = self.next_byte_timeout(KEY_WAIT)?; 512 | while seq_last != b'M' && seq_last != b'~' { 513 | str_buf.push(seq_last as char); 514 | seq_last = self.next_byte_timeout(KEY_WAIT)?; 515 | } 516 | 517 | match seq_last { 518 | b'M' => { 519 | // rxvt mouse encoding: 520 | // ESC [ Cb ; Cx ; Cy ; M 521 | let mut nums = str_buf.split(';'); 522 | 523 | let cb = nums.next().unwrap().parse::().unwrap(); 524 | let cx = nums.next().unwrap().parse::().unwrap() - 1; // 0 based 525 | let cy = nums.next().unwrap().parse::().unwrap() - 1; // 0 based 526 | 527 | match cb { 528 | 32 => Ok(MousePress(MouseButton::Left, cy, cx)), 529 | 33 => Ok(MousePress(MouseButton::Middle, cy, cx)), 530 | 34 => Ok(MousePress(MouseButton::Right, cy, cx)), 531 | 35 => Ok(MouseRelease(cy, cx)), 532 | 64 => Ok(MouseHold(cy, cx)), 533 | 96 | 97 => Ok(MousePress(MouseButton::WheelUp, cy, cx)), 534 | _ => Err(TuikitError::UnknownSequence(format!("ESC [ {} M", str_buf))), 535 | } 536 | } 537 | b'~' => { 538 | let num: u8 = str_buf.parse().unwrap(); 539 | match num { 540 | v @ 11..=15 => Ok(F(v - 10)), 541 | v @ 17..=21 => Ok(F(v - 11)), 542 | v @ 23..=24 => Ok(F(v - 12)), 543 | 200 => Ok(BracketedPasteStart), 544 | 201 => Ok(BracketedPasteEnd), 545 | _ => Err(TuikitError::UnknownSequence(format!("ESC [ {} ~", str_buf))), 546 | } 547 | } 548 | _ => unreachable!(), 549 | } 550 | } else if seq3 == b';' { 551 | let seq4 = self.next_byte_timeout(KEY_WAIT)?; 552 | if seq4 >= b'0' && seq4 <= b'9' { 553 | let seq5 = self.next_byte_timeout(KEY_WAIT)?; 554 | if seq2 == b'1' { 555 | match (seq4, seq5) { 556 | (b'5', b'A') => Ok(CtrlUp), 557 | (b'5', b'B') => Ok(CtrlDown), 558 | (b'5', b'C') => Ok(CtrlRight), 559 | (b'5', b'D') => Ok(CtrlLeft), 560 | (b'4', b'A') => Ok(AltShiftUp), 561 | (b'4', b'B') => Ok(AltShiftDown), 562 | (b'4', b'C') => Ok(AltShiftRight), 563 | (b'4', b'D') => Ok(AltShiftLeft), 564 | (b'3', b'H') => Ok(AltHome), 565 | (b'3', b'F') => Ok(AltEnd), 566 | (b'2', b'A') => Ok(ShiftUp), 567 | (b'2', b'B') => Ok(ShiftDown), 568 | (b'2', b'C') => Ok(ShiftRight), 569 | (b'2', b'D') => Ok(ShiftLeft), 570 | _ => Err(TuikitError::UnknownSequence(format!( 571 | "ESC [ 1 ; {:x?} {:x?}", 572 | seq4, seq5 573 | ))), 574 | } 575 | } else { 576 | Err(TuikitError::UnknownSequence(format!( 577 | "ESC [ {:x?} ; {:x?} {:x?}", 578 | seq2, seq4, seq5 579 | ))) 580 | } 581 | } else { 582 | Err(TuikitError::UnknownSequence(format!( 583 | "ESC [ {:x?} ; {:x?}", 584 | seq2, seq4 585 | ))) 586 | } 587 | } else { 588 | match (seq2, seq3) { 589 | (b'5', b'A') => Ok(CtrlUp), 590 | (b'5', b'B') => Ok(CtrlDown), 591 | (b'5', b'C') => Ok(CtrlRight), 592 | (b'5', b'D') => Ok(CtrlLeft), 593 | _ => Err(TuikitError::UnknownSequence(format!( 594 | "ESC [ {:x?} {:x?}", 595 | seq2, seq3 596 | ))), 597 | } 598 | } 599 | } 600 | 601 | // SSS3 602 | fn escape_o(&mut self) -> Result { 603 | let seq2 = self.next_byte_timeout(KEY_WAIT)?; 604 | match seq2 { 605 | b'A' => Ok(Up), // kcuu1 606 | b'B' => Ok(Down), // kcud1 607 | b'C' => Ok(Right), // kcuf1 608 | b'D' => Ok(Left), // kcub1 609 | b'F' => Ok(End), // kend 610 | b'H' => Ok(Home), // khome 611 | b'P' => Ok(F(1)), // kf1 612 | b'Q' => Ok(F(2)), // kf2 613 | b'R' => Ok(F(3)), // kf3 614 | b'S' => Ok(F(4)), // kf4 615 | b'a' => Ok(CtrlUp), 616 | b'b' => Ok(CtrlDown), 617 | b'c' => Ok(CtrlRight), // rxvt 618 | b'd' => Ok(CtrlLeft), // rxvt 619 | _ => Err(TuikitError::UnknownSequence(format!("ESC O {:x?}", seq2))), 620 | } 621 | } 622 | } 623 | 624 | pub struct KeyboardHandler { 625 | handler: Arc>, 626 | } 627 | 628 | impl KeyboardHandler { 629 | pub fn interrupt(&self) { 630 | let mut handler = self.handler.lock(); 631 | let _ = handler.write_all(b"x"); 632 | let _ = handler.flush(); 633 | } 634 | } 635 | -------------------------------------------------------------------------------- /src/widget/win.rs: -------------------------------------------------------------------------------- 1 | use super::split::Split; 2 | use super::util::adjust_event; 3 | use super::Size; 4 | use super::{Rectangle, Widget}; 5 | use crate::attr::Attr; 6 | use crate::canvas::{BoundedCanvas, Canvas}; 7 | use crate::cell::Cell; 8 | use crate::draw::{Draw, DrawResult}; 9 | use crate::event::Event; 10 | use crate::widget::align::{AlignSelf, HorizontalAlign}; 11 | use crate::{ok_or_return, some_or_return}; 12 | use std::cmp::max; 13 | use unicode_width::UnicodeWidthStr; 14 | 15 | type FnDrawHeader = dyn Fn(&mut dyn Canvas) -> DrawResult<()>; 16 | 17 | ///! A Win is like a div in HTML, it has its margin/padding, and border 18 | pub struct Win<'a, Message = ()> { 19 | margin_top: Size, 20 | margin_right: Size, 21 | margin_bottom: Size, 22 | margin_left: Size, 23 | 24 | padding_top: Size, 25 | padding_right: Size, 26 | padding_bottom: Size, 27 | padding_left: Size, 28 | 29 | border_top: bool, 30 | border_right: bool, 31 | border_bottom: bool, 32 | border_left: bool, 33 | 34 | border_top_attr: Attr, 35 | border_right_attr: Attr, 36 | border_bottom_attr: Attr, 37 | border_left_attr: Attr, 38 | 39 | fn_draw_header: Option>, 40 | title: Option, 41 | title_attr: Attr, 42 | right_prompt: Option, 43 | right_prompt_attr: Attr, 44 | title_align: HorizontalAlign, 45 | title_on_top: bool, 46 | 47 | basis: Size, 48 | grow: usize, 49 | shrink: usize, 50 | 51 | inner: Box + 'a>, 52 | } 53 | 54 | // Builder 55 | impl<'a, Message> Win<'a, Message> { 56 | pub fn new(widget: impl Widget + 'a) -> Self { 57 | Self { 58 | margin_top: Default::default(), 59 | margin_right: Default::default(), 60 | margin_bottom: Default::default(), 61 | margin_left: Default::default(), 62 | padding_top: Default::default(), 63 | padding_right: Default::default(), 64 | padding_bottom: Default::default(), 65 | padding_left: Default::default(), 66 | border_top: false, 67 | border_right: false, 68 | border_bottom: false, 69 | border_left: false, 70 | border_top_attr: Default::default(), 71 | border_right_attr: Default::default(), 72 | border_bottom_attr: Default::default(), 73 | border_left_attr: Default::default(), 74 | fn_draw_header: None, 75 | title: None, 76 | title_attr: Default::default(), 77 | right_prompt: None, 78 | right_prompt_attr: Default::default(), 79 | title_align: HorizontalAlign::Left, 80 | title_on_top: true, 81 | basis: Size::Default, 82 | grow: 1, 83 | shrink: 1, 84 | inner: Box::new(widget), 85 | } 86 | } 87 | 88 | pub fn margin_top(mut self, margin_top: impl Into) -> Self { 89 | self.margin_top = margin_top.into(); 90 | self 91 | } 92 | 93 | pub fn margin_right(mut self, margin_right: impl Into) -> Self { 94 | self.margin_right = margin_right.into(); 95 | self 96 | } 97 | 98 | pub fn margin_bottom(mut self, margin_bottom: impl Into) -> Self { 99 | self.margin_bottom = margin_bottom.into(); 100 | self 101 | } 102 | 103 | pub fn margin_left(mut self, margin_left: impl Into) -> Self { 104 | self.margin_left = margin_left.into(); 105 | self 106 | } 107 | 108 | pub fn margin(mut self, margin: impl Into) -> Self { 109 | let margin = margin.into(); 110 | self.margin_top = margin; 111 | self.margin_right = margin; 112 | self.margin_bottom = margin; 113 | self.margin_left = margin; 114 | self 115 | } 116 | 117 | pub fn padding_top(mut self, padding_top: impl Into) -> Self { 118 | self.padding_top = padding_top.into(); 119 | self 120 | } 121 | 122 | pub fn padding_right(mut self, padding_right: impl Into) -> Self { 123 | self.padding_right = padding_right.into(); 124 | self 125 | } 126 | 127 | pub fn padding_bottom(mut self, padding_bottom: impl Into) -> Self { 128 | self.padding_bottom = padding_bottom.into(); 129 | self 130 | } 131 | 132 | pub fn padding_left(mut self, padding_left: impl Into) -> Self { 133 | self.padding_left = padding_left.into(); 134 | self 135 | } 136 | 137 | pub fn padding(mut self, padding: impl Into) -> Self { 138 | let padding = padding.into(); 139 | self.padding_top = padding; 140 | self.padding_right = padding; 141 | self.padding_bottom = padding; 142 | self.padding_left = padding; 143 | self 144 | } 145 | 146 | pub fn border_top(mut self, border_top: bool) -> Self { 147 | self.border_top = border_top; 148 | self 149 | } 150 | 151 | pub fn border_right(mut self, border_right: bool) -> Self { 152 | self.border_right = border_right; 153 | self 154 | } 155 | 156 | pub fn border_bottom(mut self, border_bottom: bool) -> Self { 157 | self.border_bottom = border_bottom; 158 | self 159 | } 160 | 161 | pub fn border_left(mut self, border_left: bool) -> Self { 162 | self.border_left = border_left; 163 | self 164 | } 165 | 166 | pub fn border(mut self, border: bool) -> Self { 167 | self.border_top = border; 168 | self.border_right = border; 169 | self.border_bottom = border; 170 | self.border_left = border; 171 | self 172 | } 173 | 174 | pub fn border_top_attr(mut self, border_top_attr: impl Into) -> Self { 175 | self.border_top_attr = border_top_attr.into(); 176 | self 177 | } 178 | 179 | pub fn border_right_attr(mut self, border_right_attr: impl Into) -> Self { 180 | self.border_right_attr = border_right_attr.into(); 181 | self 182 | } 183 | 184 | pub fn border_bottom_attr(mut self, border_bottom_attr: impl Into) -> Self { 185 | self.border_bottom_attr = border_bottom_attr.into(); 186 | self 187 | } 188 | 189 | pub fn border_left_attr(mut self, border_left_attr: impl Into) -> Self { 190 | self.border_left_attr = border_left_attr.into(); 191 | self 192 | } 193 | 194 | pub fn border_attr(mut self, attr: impl Into) -> Self { 195 | let attr = attr.into(); 196 | self.border_top_attr = attr; 197 | self.border_right_attr = attr; 198 | self.border_bottom_attr = attr; 199 | self.border_left_attr = attr; 200 | self 201 | } 202 | 203 | pub fn fn_draw_header(mut self, fn_draw_header: Box) -> Self { 204 | self.fn_draw_header = Some(fn_draw_header); 205 | self 206 | } 207 | 208 | pub fn title(mut self, title: impl Into) -> Self { 209 | self.title = Some(title.into()); 210 | self 211 | } 212 | 213 | pub fn title_attr(mut self, title_attr: impl Into) -> Self { 214 | self.title_attr = title_attr.into(); 215 | self 216 | } 217 | 218 | pub fn right_prompt(mut self, right_prompt: impl Into) -> Self { 219 | self.right_prompt = Some(right_prompt.into()); 220 | self 221 | } 222 | 223 | pub fn right_prompt_attr(mut self, right_prompt_attr: impl Into) -> Self { 224 | self.right_prompt_attr = right_prompt_attr.into(); 225 | self 226 | } 227 | 228 | pub fn title_align(mut self, align: HorizontalAlign) -> Self { 229 | self.title_align = align; 230 | self 231 | } 232 | 233 | pub fn title_on_top(mut self, title_on_top: bool) -> Self { 234 | self.title_on_top = title_on_top; 235 | self 236 | } 237 | 238 | pub fn basis(mut self, basis: impl Into) -> Self { 239 | self.basis = basis.into(); 240 | self 241 | } 242 | 243 | pub fn grow(mut self, grow: usize) -> Self { 244 | self.grow = grow; 245 | self 246 | } 247 | 248 | pub fn shrink(mut self, shrink: usize) -> Self { 249 | self.shrink = shrink; 250 | self 251 | } 252 | } 253 | 254 | impl<'a, Message> Win<'a, Message> { 255 | fn rect_reserve_margin(&self, rect: Rectangle) -> DrawResult { 256 | let Rectangle { width, height, .. } = rect; 257 | 258 | let margin_top = self.margin_top.calc_fixed_size(height, 0); 259 | let margin_right = self.margin_right.calc_fixed_size(width, 0); 260 | let margin_bottom = self.margin_bottom.calc_fixed_size(height, 0); 261 | let margin_left = self.margin_left.calc_fixed_size(width, 0); 262 | 263 | if margin_top + margin_bottom >= height || margin_left + margin_right >= width { 264 | return Err("margin takes too much screen".into()); 265 | } 266 | 267 | let top = margin_top; 268 | let left = margin_left; 269 | let width = width - (margin_left + margin_right); 270 | let height = height - (margin_top + margin_bottom); 271 | Ok(Rectangle { 272 | top, 273 | left, 274 | width, 275 | height, 276 | }) 277 | } 278 | 279 | fn rect_header(&self, rect_reserve_margin: Rectangle) -> Rectangle { 280 | let Rectangle { 281 | top, 282 | mut left, 283 | width, 284 | height, 285 | } = rect_reserve_margin; 286 | 287 | let new_top = if self.title_on_top { 288 | top 289 | } else { 290 | max(top + height, 1) - 1 291 | }; 292 | 293 | let height_needed = if self.title_on_top && self.border_bottom { 294 | 2 295 | } else { 296 | 1 297 | }; 298 | if height_needed > height { 299 | // not enough space, don't draw at all 300 | return Rectangle { 301 | top: new_top, 302 | left, 303 | width, 304 | height: 0, 305 | }; 306 | } 307 | 308 | let mut width_needed = 0; 309 | if self.border_left { 310 | width_needed += 1; 311 | left += 1; 312 | } 313 | if self.border_right { 314 | width_needed += 1; 315 | } 316 | if width_needed > width { 317 | return Rectangle { 318 | top: new_top, 319 | left, 320 | width: 0, 321 | height, 322 | }; 323 | } 324 | 325 | Rectangle { 326 | top: new_top, 327 | left, 328 | width: width - width_needed, 329 | height: 1, 330 | } 331 | } 332 | 333 | fn rect_reserve_border(&self, rect: Rectangle) -> DrawResult { 334 | let Rectangle { 335 | top, 336 | left, 337 | width, 338 | height, 339 | } = rect; 340 | 341 | // title and right prompt will be displayed on top 342 | let border_top = self.border_top 343 | || (self.title_on_top && (self.title.is_some() || self.right_prompt.is_some())); 344 | let border_bottom = self.border_bottom 345 | || (!self.title_on_top && (self.title.is_some() || self.right_prompt.is_some())); 346 | 347 | if border_top || border_bottom { 348 | if (height < 1) || (border_top && border_bottom && height < 2) { 349 | return Err("not enough height for border".into()); 350 | } 351 | } 352 | 353 | if self.border_left || self.border_right { 354 | if (width < 1) || (self.border_left && self.border_right && width < 2) { 355 | return Err("not enough width for border".into()); 356 | } 357 | } 358 | 359 | let top = if border_top { top + 1 } else { top }; 360 | let left = if self.border_left { left + 1 } else { left }; 361 | let width = if self.border_left { width - 1 } else { width }; 362 | let width = if self.border_right { width - 1 } else { width }; 363 | let height = if border_top { height - 1 } else { height }; 364 | let height = if border_bottom { height - 1 } else { height }; 365 | 366 | Ok(Rectangle { 367 | top, 368 | left, 369 | width, 370 | height, 371 | }) 372 | } 373 | 374 | fn rect_reserve_padding(&self, rect: Rectangle) -> DrawResult { 375 | let Rectangle { 376 | top, 377 | left, 378 | width, 379 | height, 380 | } = rect; 381 | 382 | let padding_top = self.padding_top.calc_fixed_size(height, 0); 383 | let padding_right = self.padding_right.calc_fixed_size(width, 0); 384 | let padding_bottom = self.padding_bottom.calc_fixed_size(height, 0); 385 | let padding_left = self.padding_left.calc_fixed_size(width, 0); 386 | 387 | if padding_top + padding_bottom >= height || padding_left + padding_right >= width { 388 | return Err("padding takes too much screen, won't draw".into()); 389 | } 390 | 391 | let top = top + padding_top; 392 | let left = left + padding_left; 393 | let width = width - (padding_left + padding_right); 394 | let height = height - (padding_top + padding_bottom); 395 | Ok(Rectangle { 396 | top, 397 | left, 398 | width, 399 | height, 400 | }) 401 | } 402 | 403 | /// Calculate the inner rectangle(inside margin, border, padding) 404 | fn calc_inner_rect(&self, rect: Rectangle) -> DrawResult { 405 | self.rect_reserve_padding(self.rect_reserve_border(self.rect_reserve_margin(rect)?)?) 406 | } 407 | 408 | /// draw border and return the position & size of the inner canvas 409 | /// (top, left, width, height) 410 | fn draw_border(&self, rect: Rectangle, canvas: &mut dyn Canvas) -> DrawResult<()> { 411 | let Rectangle { 412 | top, 413 | left, 414 | width, 415 | height, 416 | } = rect; 417 | 418 | if self.border_top || self.border_bottom { 419 | if (height < 1) || (self.border_top && self.border_bottom && height < 2) { 420 | return Err("not enough height for border".into()); 421 | } 422 | } 423 | 424 | if self.border_left || self.border_right { 425 | if (width < 1) || (self.border_left && self.border_right && width < 2) { 426 | return Err("not enough width for border".into()); 427 | } 428 | } 429 | 430 | let bottom = max(top + height, 1) - 1; 431 | let right = max(left + width, 1) - 1; 432 | 433 | if self.border_top { 434 | let _ = canvas.print_with_attr(top, left, &"─".repeat(width), self.border_top_attr); 435 | } 436 | 437 | if self.border_bottom { 438 | let _ = 439 | canvas.print_with_attr(bottom, left, &"─".repeat(width), self.border_bottom_attr); 440 | } 441 | 442 | if self.border_left { 443 | for i in top..(top + height) { 444 | let _ = canvas.print_with_attr(i, left, "│", self.border_left_attr); 445 | } 446 | } 447 | 448 | if self.border_right { 449 | for i in top..(top + height) { 450 | let _ = canvas.print_with_attr(i, right, "│", self.border_right_attr); 451 | } 452 | } 453 | 454 | // draw 4 corners if necessary 455 | 456 | if self.border_top && self.border_left { 457 | let _ = canvas.put_cell( 458 | top, 459 | left, 460 | Cell::default().ch('┌').attribute(self.border_top_attr), 461 | ); 462 | } 463 | 464 | if self.border_top && self.border_right { 465 | let _ = canvas.put_cell( 466 | top, 467 | right, 468 | Cell::default().ch('┐').attribute(self.border_top_attr), 469 | ); 470 | } 471 | 472 | if self.border_bottom && self.border_left { 473 | let _ = canvas.put_cell( 474 | bottom, 475 | left, 476 | Cell::default().ch('└').attribute(self.border_bottom_attr), 477 | ); 478 | } 479 | 480 | if self.border_bottom && self.border_right { 481 | let _ = canvas.put_cell( 482 | bottom, 483 | right, 484 | Cell::default().ch('┘').attribute(self.border_bottom_attr), 485 | ); 486 | } 487 | 488 | Ok(()) 489 | } 490 | 491 | fn draw_title_and_prompt(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 492 | let (width, height) = canvas.size()?; 493 | let row = if self.title_on_top { 494 | 0 495 | } else { 496 | max(height, 1) - 1 497 | }; 498 | 499 | if self.right_prompt.is_some() { 500 | let prompt = self.right_prompt.as_ref().unwrap(); 501 | let text_width = prompt.width_cjk(); 502 | let left = HorizontalAlign::Right.adjust(0, width, text_width); 503 | canvas.print_with_attr(row, left, prompt, self.right_prompt_attr)?; 504 | } 505 | 506 | if self.title.is_some() { 507 | let title = self.title.as_ref().unwrap(); 508 | let text_width = title.width_cjk(); 509 | let left = self.title_align.adjust(0, width, text_width); 510 | canvas.print_with_attr(row, left, title, self.right_prompt_attr)?; 511 | } 512 | 513 | Ok(()) 514 | } 515 | 516 | fn draw_header(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 517 | let (width, height) = canvas.size()?; 518 | if width <= 0 || height <= 0 { 519 | return Ok(()); 520 | } 521 | 522 | if self.fn_draw_header.is_some() { 523 | self.fn_draw_header.as_ref().unwrap()(canvas)?; 524 | } else { 525 | self.draw_title_and_prompt(canvas)?; 526 | } 527 | 528 | Ok(()) 529 | } 530 | 531 | fn draw_context(&self, canvas: &'a mut dyn Canvas) -> DrawResult> { 532 | let (width, height) = canvas.size()?; 533 | let outer_rect = Rectangle { 534 | top: 0, 535 | left: 0, 536 | width, 537 | height, 538 | }; 539 | 540 | let rect_in_margin = self.rect_reserve_margin(outer_rect)?; 541 | self.draw_border(rect_in_margin, canvas)?; 542 | 543 | let Rectangle { 544 | top, 545 | left, 546 | width, 547 | height, 548 | } = self.rect_header(rect_in_margin); 549 | let mut header_canvas = BoundedCanvas::new(top, left, width, height, canvas); 550 | self.draw_header(&mut header_canvas)?; 551 | 552 | let Rectangle { 553 | top, 554 | left, 555 | width, 556 | height, 557 | } = self.calc_inner_rect(outer_rect)?; 558 | 559 | Ok(BoundedCanvas::new(top, left, width, height, canvas)) 560 | } 561 | } 562 | 563 | impl<'a, Message> Draw for Win<'a, Message> { 564 | /// Reserve margin & padding, draw border. 565 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 566 | let mut new_canvas = self.draw_context(canvas)?; 567 | self.inner.draw(&mut new_canvas) 568 | } 569 | 570 | fn draw_mut(&mut self, canvas: &mut dyn Canvas) -> DrawResult<()> { 571 | let mut new_canvas = self.draw_context(canvas)?; 572 | self.inner.draw_mut(&mut new_canvas) 573 | } 574 | } 575 | 576 | impl<'a, Message> Widget for Win<'a, Message> { 577 | fn size_hint(&self) -> (Option, Option) { 578 | // plus border size 579 | let (width, height) = self.inner.size_hint(); 580 | let width = width.map(|mut w| { 581 | w += if self.border_left { 1 } else { 0 }; 582 | w += if self.border_right { 1 } else { 0 }; 583 | w 584 | }); 585 | 586 | let height = height.map(|mut h| { 587 | h += if self.border_top { 1 } else { 0 }; 588 | h += if self.border_bottom { 1 } else { 0 }; 589 | h 590 | }); 591 | 592 | (width, height) 593 | } 594 | 595 | fn on_event(&self, event: Event, rect: Rectangle) -> Vec { 596 | let empty = vec![]; 597 | let inner_rect = ok_or_return!(self.calc_inner_rect(rect), empty); 598 | let adjusted_event = some_or_return!(adjust_event(event, inner_rect), empty); 599 | self.inner.on_event(adjusted_event, inner_rect) 600 | } 601 | 602 | fn on_event_mut(&mut self, event: Event, rect: Rectangle) -> Vec { 603 | let empty = vec![]; 604 | let inner_rect = ok_or_return!(self.calc_inner_rect(rect), empty); 605 | let adjusted_event = some_or_return!(adjust_event(event, inner_rect), empty); 606 | self.inner.on_event(adjusted_event, inner_rect) 607 | } 608 | } 609 | 610 | impl<'a, Message> Split for Win<'a, Message> { 611 | fn get_basis(&self) -> Size { 612 | self.basis 613 | } 614 | 615 | fn get_grow(&self) -> usize { 616 | self.grow 617 | } 618 | 619 | fn get_shrink(&self) -> usize { 620 | self.shrink 621 | } 622 | } 623 | 624 | #[cfg(test)] 625 | #[allow(dead_code)] 626 | mod test { 627 | use super::*; 628 | use std::sync::Mutex; 629 | 630 | struct WinHint { 631 | pub width_hint: Option, 632 | pub height_hint: Option, 633 | } 634 | 635 | impl Draw for WinHint { 636 | fn draw(&self, _canvas: &mut dyn Canvas) -> DrawResult<()> { 637 | unimplemented!() 638 | } 639 | } 640 | 641 | impl Widget for WinHint { 642 | fn size_hint(&self) -> (Option, Option) { 643 | (self.width_hint, self.height_hint) 644 | } 645 | } 646 | 647 | #[test] 648 | fn size_hint_for_window_should_include_border() { 649 | let inner = WinHint { 650 | width_hint: None, 651 | height_hint: None, 652 | }; 653 | let win_border_top = Win::new(&inner).border_top(true); 654 | assert_eq!((None, None), win_border_top.size_hint()); 655 | let win_border_right = Win::new(&inner).border_right(true); 656 | assert_eq!((None, None), win_border_right.size_hint()); 657 | let win_border_bottom = Win::new(&inner).border_bottom(true); 658 | assert_eq!((None, None), win_border_bottom.size_hint()); 659 | let win_border_left = Win::new(&inner).border_left(true); 660 | assert_eq!((None, None), win_border_left.size_hint()); 661 | 662 | let inner = WinHint { 663 | width_hint: Some(1), 664 | height_hint: None, 665 | }; 666 | let win_border_top = Win::new(&inner).border_top(true); 667 | assert_eq!((Some(1), None), win_border_top.size_hint()); 668 | let win_border_right = Win::new(&inner).border_right(true); 669 | assert_eq!((Some(2), None), win_border_right.size_hint()); 670 | let win_border_bottom = Win::new(&inner).border_bottom(true); 671 | assert_eq!((Some(1), None), win_border_bottom.size_hint()); 672 | let win_border_left = Win::new(&inner).border_left(true); 673 | assert_eq!((Some(2), None), win_border_left.size_hint()); 674 | 675 | let inner = WinHint { 676 | width_hint: None, 677 | height_hint: Some(1), 678 | }; 679 | let win_border_top = Win::new(&inner).border_top(true); 680 | assert_eq!((None, Some(2)), win_border_top.size_hint()); 681 | let win_border_right = Win::new(&inner).border_right(true); 682 | assert_eq!((None, Some(1)), win_border_right.size_hint()); 683 | let win_border_bottom = Win::new(&inner).border_bottom(true); 684 | assert_eq!((None, Some(2)), win_border_bottom.size_hint()); 685 | let win_border_left = Win::new(&inner).border_left(true); 686 | assert_eq!((None, Some(1)), win_border_left.size_hint()); 687 | } 688 | 689 | #[derive(PartialEq, Debug)] 690 | enum Called { 691 | No, 692 | Mut, 693 | Immut, 694 | } 695 | 696 | struct Drawn { 697 | called: Mutex, 698 | } 699 | 700 | impl Draw for Drawn { 701 | fn draw(&self, _canvas: &mut dyn Canvas) -> DrawResult<()> { 702 | *self.called.lock().unwrap() = Called::Immut; 703 | Ok(()) 704 | } 705 | fn draw_mut(&mut self, _canvas: &mut dyn Canvas) -> DrawResult<()> { 706 | *self.called.lock().unwrap() = Called::Mut; 707 | Ok(()) 708 | } 709 | } 710 | 711 | impl Widget for Drawn {} 712 | 713 | #[derive(Default)] 714 | struct TestCanvas {} 715 | 716 | #[allow(unused_variables)] 717 | impl Canvas for TestCanvas { 718 | fn size(&self) -> crate::Result<(usize, usize)> { 719 | Ok((100, 100)) 720 | } 721 | 722 | fn clear(&mut self) -> crate::Result<()> { 723 | unimplemented!() 724 | } 725 | 726 | fn put_cell(&mut self, row: usize, col: usize, cell: Cell) -> crate::Result { 727 | Ok(1) 728 | } 729 | 730 | fn set_cursor(&mut self, row: usize, col: usize) -> crate::Result<()> { 731 | unimplemented!() 732 | } 733 | 734 | fn show_cursor(&mut self, show: bool) -> crate::Result<()> { 735 | unimplemented!() 736 | } 737 | } 738 | 739 | #[test] 740 | fn mutable_widget() { 741 | let mut canvas = TestCanvas::default(); 742 | 743 | let mut mutable = Drawn { 744 | called: Mutex::new(Called::No), 745 | }; 746 | { 747 | let mut win = Win::new(&mut mutable); 748 | let _ = win.draw_mut(&mut canvas).unwrap(); 749 | } 750 | assert_eq!(Called::Mut, *mutable.called.lock().unwrap()); 751 | 752 | let immutable = Drawn { 753 | called: Mutex::new(Called::No), 754 | }; 755 | let win = Win::new(&immutable); 756 | let _ = win.draw(&mut canvas).unwrap(); 757 | assert_eq!(Called::Immut, *immutable.called.lock().unwrap()); 758 | } 759 | } 760 | -------------------------------------------------------------------------------- /src/term.rs: -------------------------------------------------------------------------------- 1 | //! Term is a thread-safe "terminal". 2 | //! 3 | //! It allows you to: 4 | //! - Listen to key stroke events 5 | //! - Output contents to the terminal 6 | //! 7 | //! ```no_run 8 | //! use tuikit::prelude::*; 9 | //! 10 | //! let term = Term::<()>::new().unwrap(); 11 | //! 12 | //! while let Ok(ev) = term.poll_event() { 13 | //! if let Event::Key(Key::Char('q')) = ev { 14 | //! break; 15 | //! } 16 | //! 17 | //! term.print(0, 0, format!("got event: {:?}", ev).as_str()); 18 | //! term.present(); 19 | //! } 20 | //! ``` 21 | //! 22 | //! Term is modeled after [termbox](https://github.com/nsf/termbox). The main idea is viewing 23 | //! terminals as a table of fixed-size cells and input being a stream of structured messages 24 | 25 | use std::cmp::{max, min}; 26 | use std::sync::atomic::{AtomicUsize, Ordering}; 27 | use std::sync::mpsc::{channel, Receiver, Sender}; 28 | use std::sync::Arc; 29 | use std::thread; 30 | use std::time::Duration; 31 | 32 | use crate::attr::Attr; 33 | use crate::canvas::Canvas; 34 | use crate::cell::Cell; 35 | use crate::draw::Draw; 36 | use crate::error::TuikitError; 37 | use crate::event::Event; 38 | use crate::input::{KeyBoard, KeyboardHandler}; 39 | use crate::key::Key; 40 | use crate::output::Command; 41 | use crate::output::Output; 42 | use crate::raw::{get_tty, IntoRawMode}; 43 | use crate::screen::Screen; 44 | use crate::spinlock::SpinLock; 45 | use crate::sys::signal::{initialize_signals, notify_on_sigwinch, unregister_sigwinch}; 46 | use crate::Result; 47 | 48 | const MIN_HEIGHT: usize = 1; 49 | const WAIT_TIMEOUT: Duration = Duration::from_millis(300); 50 | const POLLING_TIMEOUT: Duration = Duration::from_millis(10); 51 | 52 | #[derive(Debug, Copy, Clone)] 53 | pub enum TermHeight { 54 | Fixed(usize), 55 | Percent(usize), 56 | } 57 | 58 | pub struct Term { 59 | components_to_stop: Arc, 60 | keyboard_handler: SpinLock>, 61 | resize_signal_id: Arc, 62 | term_lock: SpinLock, 63 | event_rx: SpinLock>>, 64 | event_tx: Arc>>>, 65 | raw_mouse: bool, // to produce raw mouse event or the parsed event(e.g. DoubleClick) 66 | } 67 | 68 | pub struct TermOptions { 69 | max_height: TermHeight, 70 | min_height: TermHeight, 71 | height: TermHeight, 72 | clear_on_exit: bool, 73 | clear_on_start: bool, 74 | mouse_enabled: bool, 75 | raw_mouse: bool, 76 | hold: bool, // to start term or not on creation 77 | disable_alternate_screen: bool, 78 | } 79 | 80 | impl Default for TermOptions { 81 | fn default() -> Self { 82 | Self { 83 | max_height: TermHeight::Percent(100), 84 | min_height: TermHeight::Fixed(3), 85 | height: TermHeight::Percent(100), 86 | clear_on_exit: true, 87 | clear_on_start: true, 88 | mouse_enabled: false, 89 | raw_mouse: false, 90 | hold: false, 91 | disable_alternate_screen: false, 92 | } 93 | } 94 | } 95 | 96 | // Builder 97 | impl TermOptions { 98 | pub fn max_height(mut self, max_height: TermHeight) -> Self { 99 | self.max_height = max_height; 100 | self 101 | } 102 | 103 | pub fn min_height(mut self, min_height: TermHeight) -> Self { 104 | self.min_height = min_height; 105 | self 106 | } 107 | pub fn height(mut self, height: TermHeight) -> Self { 108 | self.height = height; 109 | self 110 | } 111 | pub fn clear_on_exit(mut self, clear: bool) -> Self { 112 | self.clear_on_exit = clear; 113 | self 114 | } 115 | pub fn clear_on_start(mut self, clear: bool) -> Self { 116 | self.clear_on_start = clear; 117 | self 118 | } 119 | pub fn mouse_enabled(mut self, enabled: bool) -> Self { 120 | self.mouse_enabled = enabled; 121 | self 122 | } 123 | pub fn raw_mouse(mut self, enabled: bool) -> Self { 124 | self.raw_mouse = enabled; 125 | self 126 | } 127 | pub fn hold(mut self, hold: bool) -> Self { 128 | self.hold = hold; 129 | self 130 | } 131 | pub fn disable_alternate_screen(mut self, disable_alternate_screen: bool) -> Self { 132 | self.disable_alternate_screen = disable_alternate_screen; 133 | self 134 | } 135 | } 136 | 137 | impl Term { 138 | /// Create a Term with height specified. 139 | /// 140 | /// Internally if the calculated height would fill the whole screen, `Alternate Screen` will 141 | /// be enabled, otherwise only part of the screen will be used. 142 | /// 143 | /// If the preferred height is larger than the current screen, whole screen is used. 144 | /// 145 | /// ```no_run 146 | /// use tuikit::term::{Term, TermHeight}; 147 | /// 148 | /// let term: Term<()> = Term::with_height(TermHeight::Percent(30)).unwrap(); // 30% of the terminal height 149 | /// let term: Term<()> = Term::with_height(TermHeight::Fixed(20)).unwrap(); // fixed 20 lines 150 | /// ``` 151 | pub fn with_height(height: TermHeight) -> Result> { 152 | Term::with_options(TermOptions::default().height(height)) 153 | } 154 | 155 | /// Create a Term (with 100% height) 156 | /// 157 | /// ```no_run 158 | /// use tuikit::term::{Term, TermHeight}; 159 | /// 160 | /// let term: Term<()> = Term::new().unwrap(); 161 | /// let term: Term<()> = Term::with_height(TermHeight::Percent(100)).unwrap(); 162 | /// ``` 163 | pub fn new() -> Result> { 164 | Term::with_options(TermOptions::default()) 165 | } 166 | 167 | /// Create a Term with custom options 168 | /// 169 | /// ```no_run 170 | /// use tuikit::term::{Term, TermHeight, TermOptions}; 171 | /// 172 | /// let term: Term<()> = Term::with_options(TermOptions::default().height(TermHeight::Percent(100))).unwrap(); 173 | /// ``` 174 | pub fn with_options(options: TermOptions) -> Result> { 175 | initialize_signals(); 176 | 177 | let (event_tx, event_rx) = channel(); 178 | let raw_mouse = options.raw_mouse; 179 | let ret = Term { 180 | components_to_stop: Arc::new(AtomicUsize::new(0)), 181 | keyboard_handler: SpinLock::new(None), 182 | resize_signal_id: Arc::new(AtomicUsize::new(0)), 183 | term_lock: SpinLock::new(TermLock::with_options(&options)), 184 | event_tx: Arc::new(SpinLock::new(event_tx)), 185 | event_rx: SpinLock::new(event_rx), 186 | raw_mouse, 187 | }; 188 | if options.hold { 189 | Ok(ret) 190 | } else { 191 | ret.restart().map(|_| ret) 192 | } 193 | } 194 | 195 | fn ensure_not_stopped(&self) -> Result<()> { 196 | if self.components_to_stop.load(Ordering::SeqCst) == 2 { 197 | Ok(()) 198 | } else { 199 | Err(TuikitError::TerminalNotStarted) 200 | } 201 | } 202 | 203 | fn get_cursor_pos( 204 | &self, 205 | keyboard: &mut KeyBoard, 206 | output: &mut Output, 207 | ) -> Result<(usize, usize)> { 208 | output.ask_for_cpr(); 209 | 210 | if let Ok(key) = keyboard.next_key_timeout(WAIT_TIMEOUT) { 211 | if let Key::CursorPos(row, col) = key { 212 | return Ok((row as usize, col as usize)); 213 | } 214 | } 215 | 216 | Ok((0, 0)) 217 | } 218 | 219 | /// restart the terminal if it had been stopped 220 | pub fn restart(&self) -> Result<()> { 221 | let mut termlock = self.term_lock.lock(); 222 | if self.components_to_stop.load(Ordering::SeqCst) == 2 { 223 | return Ok(()); 224 | } 225 | 226 | let ttyout = get_tty()?.into_raw_mode()?; 227 | let mut output = Output::new(Box::new(ttyout))?; 228 | let mut keyboard = KeyBoard::new_with_tty().raw_mouse(self.raw_mouse); 229 | self.keyboard_handler 230 | .lock() 231 | .replace(keyboard.get_interrupt_handler()); 232 | let cursor_pos = self.get_cursor_pos(&mut keyboard, &mut output)?; 233 | termlock.restart(output, cursor_pos)?; 234 | 235 | // start two listener 236 | self.start_key_listener(keyboard); 237 | self.start_size_change_listener(); 238 | 239 | // wait for components to start 240 | while self.components_to_stop.load(Ordering::SeqCst) < 2 { 241 | debug!( 242 | "restart: components: {}", 243 | self.components_to_stop.load(Ordering::SeqCst) 244 | ); 245 | thread::sleep(POLLING_TIMEOUT); 246 | } 247 | 248 | let event_tx = self.event_tx.lock(); 249 | let _ = event_tx.send(Event::Restarted); 250 | 251 | Ok(()) 252 | } 253 | 254 | /// Pause the Term 255 | /// 256 | /// This function will cause the Term to give away the control to the terminal(such as listening 257 | /// to the key strokes). After the Term was "paused", `poll_event` will block indefinitely and 258 | /// recover after the Term was `restart`ed. 259 | pub fn pause(&self) -> Result<()> { 260 | self.pause_internal(false) 261 | } 262 | 263 | fn pause_internal(&self, exiting: bool) -> Result<()> { 264 | debug!("pause"); 265 | let mut termlock = self.term_lock.lock(); 266 | 267 | if self.components_to_stop.load(Ordering::SeqCst) == 0 { 268 | return Ok(()); 269 | } 270 | 271 | // wait for the components to stop 272 | // i.e. key_listener & size_change_listener 273 | self.keyboard_handler.lock().take().map(|h| h.interrupt()); 274 | unregister_sigwinch(self.resize_signal_id.load(Ordering::Relaxed)).map(|tx| tx.send(())); 275 | 276 | termlock.pause(exiting)?; 277 | 278 | // wait for the components to stop 279 | while self.components_to_stop.load(Ordering::SeqCst) > 0 { 280 | debug!( 281 | "pause: components: {}", 282 | self.components_to_stop.load(Ordering::SeqCst) 283 | ); 284 | thread::sleep(POLLING_TIMEOUT); 285 | } 286 | 287 | Ok(()) 288 | } 289 | 290 | fn start_key_listener(&self, mut keyboard: KeyBoard) { 291 | let event_tx_clone = self.event_tx.clone(); 292 | let components_to_stop = self.components_to_stop.clone(); 293 | thread::spawn(move || { 294 | components_to_stop.fetch_add(1, Ordering::SeqCst); 295 | debug!("key listener start"); 296 | loop { 297 | let next_key = keyboard.next_key(); 298 | trace!("next key: {:?}", next_key); 299 | match next_key { 300 | Ok(key) => { 301 | let event_tx = event_tx_clone.lock(); 302 | let _ = event_tx.send(Event::Key(key)); 303 | } 304 | Err(TuikitError::Interrupted) => break, 305 | _ => {} // ignored 306 | } 307 | } 308 | components_to_stop.fetch_sub(1, Ordering::SeqCst); 309 | debug!("key listener stop"); 310 | }); 311 | } 312 | 313 | fn start_size_change_listener(&self) { 314 | let event_tx_clone = self.event_tx.clone(); 315 | let resize_signal_id = self.resize_signal_id.clone(); 316 | let components_to_stop = self.components_to_stop.clone(); 317 | 318 | thread::spawn(move || { 319 | let (id, sigwinch_rx) = notify_on_sigwinch(); 320 | resize_signal_id.store(id, Ordering::Relaxed); 321 | 322 | components_to_stop.fetch_add(1, Ordering::SeqCst); 323 | debug!("size change listener started"); 324 | loop { 325 | if let Ok(_) = sigwinch_rx.recv() { 326 | let event_tx = event_tx_clone.lock(); 327 | let _ = event_tx.send(Event::Resize { 328 | width: 0, 329 | height: 0, 330 | }); 331 | } else { 332 | break; 333 | } 334 | } 335 | components_to_stop.fetch_sub(1, Ordering::SeqCst); 336 | debug!("size change listener stop"); 337 | }); 338 | } 339 | 340 | fn filter_event(&self, event: Event) -> Event { 341 | match event { 342 | Event::Resize { .. } => { 343 | { 344 | let mut termlock = self.term_lock.lock(); 345 | let _ = termlock.on_resize(); 346 | } 347 | let (width, height) = self.term_size().unwrap_or((0, 0)); 348 | Event::Resize { width, height } 349 | } 350 | Event::Key(Key::MousePress(button, row, col)) => { 351 | // adjust mouse event position 352 | let cursor_row = self.term_lock.lock().get_term_start_row() as u16; 353 | if row < cursor_row { 354 | Event::__Nonexhaustive 355 | } else { 356 | Event::Key(Key::MousePress(button, row - cursor_row, col)) 357 | } 358 | } 359 | Event::Key(Key::MouseRelease(row, col)) => { 360 | // adjust mouse event position 361 | let cursor_row = self.term_lock.lock().get_term_start_row() as u16; 362 | if row < cursor_row { 363 | Event::__Nonexhaustive 364 | } else { 365 | Event::Key(Key::MouseRelease(row - cursor_row, col)) 366 | } 367 | } 368 | Event::Key(Key::MouseHold(row, col)) => { 369 | // adjust mouse event position 370 | let cursor_row = self.term_lock.lock().get_term_start_row() as u16; 371 | if row < cursor_row { 372 | Event::__Nonexhaustive 373 | } else { 374 | Event::Key(Key::MouseHold(row - cursor_row, col)) 375 | } 376 | } 377 | Event::Key(Key::SingleClick(button, row, col)) => { 378 | let cursor_row = self.term_lock.lock().get_term_start_row() as u16; 379 | if row < cursor_row { 380 | Event::__Nonexhaustive 381 | } else { 382 | Event::Key(Key::SingleClick(button, row - cursor_row, col)) 383 | } 384 | } 385 | Event::Key(Key::DoubleClick(button, row, col)) => { 386 | let cursor_row = self.term_lock.lock().get_term_start_row() as u16; 387 | if row < cursor_row { 388 | Event::__Nonexhaustive 389 | } else { 390 | Event::Key(Key::DoubleClick(button, row - cursor_row, col)) 391 | } 392 | } 393 | Event::Key(Key::WheelUp(row, col, num)) => { 394 | let cursor_row = self.term_lock.lock().get_term_start_row() as u16; 395 | if row < cursor_row { 396 | Event::__Nonexhaustive 397 | } else { 398 | Event::Key(Key::WheelUp(row - cursor_row, col, num)) 399 | } 400 | } 401 | Event::Key(Key::WheelDown(row, col, num)) => { 402 | let cursor_row = self.term_lock.lock().get_term_start_row() as u16; 403 | if row < cursor_row { 404 | Event::__Nonexhaustive 405 | } else { 406 | Event::Key(Key::WheelDown(row - cursor_row, col, num)) 407 | } 408 | } 409 | ev => ev, 410 | } 411 | } 412 | 413 | /// Wait an event up to `timeout` and return it 414 | pub fn peek_event(&self, timeout: Duration) -> Result> { 415 | let event_rx = self.event_rx.lock(); 416 | event_rx 417 | .recv_timeout(timeout) 418 | .map(|ev| self.filter_event(ev)) 419 | .map_err(|_| TuikitError::Timeout(timeout)) 420 | } 421 | 422 | /// Wait for an event indefinitely and return it 423 | pub fn poll_event(&self) -> Result> { 424 | let event_rx = self.event_rx.lock(); 425 | event_rx 426 | .recv() 427 | .map(|ev| self.filter_event(ev)) 428 | .map_err(|err| TuikitError::ChannelReceiveError(err)) 429 | } 430 | 431 | /// An interface to inject event to the terminal's event queue 432 | pub fn send_event(&self, event: Event) -> Result<()> { 433 | let event_tx = self.event_tx.lock(); 434 | event_tx 435 | .send(event) 436 | .map_err(|err| TuikitError::SendEventError(err.to_string())) 437 | } 438 | 439 | /// Sync internal buffer with terminal 440 | pub fn present(&self) -> Result<()> { 441 | self.ensure_not_stopped()?; 442 | let mut termlock = self.term_lock.lock(); 443 | termlock.present() 444 | } 445 | 446 | /// Return the printable size(width, height) of the term 447 | pub fn term_size(&self) -> Result<(usize, usize)> { 448 | self.ensure_not_stopped()?; 449 | let termlock = self.term_lock.lock(); 450 | Ok(termlock.term_size()?) 451 | } 452 | 453 | /// Clear internal buffer 454 | pub fn clear(&self) -> Result<()> { 455 | self.ensure_not_stopped()?; 456 | let mut termlock = self.term_lock.lock(); 457 | termlock.clear() 458 | } 459 | 460 | /// Change a cell of position `(row, col)` to `cell` 461 | pub fn put_cell(&self, row: usize, col: usize, cell: Cell) -> Result { 462 | self.ensure_not_stopped()?; 463 | let mut termlock = self.term_lock.lock(); 464 | termlock.put_cell(row, col, cell) 465 | } 466 | 467 | /// Print `content` starting with position `(row, col)` 468 | pub fn print(&self, row: usize, col: usize, content: &str) -> Result { 469 | self.print_with_attr(row, col, content, Attr::default()) 470 | } 471 | 472 | /// print `content` starting with position `(row, col)` with `attr` 473 | pub fn print_with_attr( 474 | &self, 475 | row: usize, 476 | col: usize, 477 | content: &str, 478 | attr: impl Into, 479 | ) -> Result { 480 | self.ensure_not_stopped()?; 481 | let mut termlock = self.term_lock.lock(); 482 | termlock.print_with_attr(row, col, content, attr) 483 | } 484 | 485 | /// Set cursor position to (row, col), and show the cursor 486 | pub fn set_cursor(&self, row: usize, col: usize) -> Result<()> { 487 | self.ensure_not_stopped()?; 488 | let mut termlock = self.term_lock.lock(); 489 | termlock.set_cursor(row, col) 490 | } 491 | 492 | /// show/hide cursor, set `show` to `false` to hide the cursor 493 | pub fn show_cursor(&self, show: bool) -> Result<()> { 494 | self.ensure_not_stopped()?; 495 | let mut termlock = self.term_lock.lock(); 496 | termlock.show_cursor(show) 497 | } 498 | 499 | /// Enable mouse support 500 | pub fn enable_mouse_support(&self) -> Result<()> { 501 | self.ensure_not_stopped()?; 502 | let mut termlock = self.term_lock.lock(); 503 | termlock.enable_mouse_support() 504 | } 505 | 506 | /// Disable mouse support 507 | pub fn disable_mouse_support(&self) -> Result<()> { 508 | self.ensure_not_stopped()?; 509 | let mut termlock = self.term_lock.lock(); 510 | termlock.disable_mouse_support() 511 | } 512 | 513 | /// Whether to clear the terminal upon exiting. Defaults to true. 514 | pub fn clear_on_exit(&self, clear: bool) -> Result<()> { 515 | self.ensure_not_stopped()?; 516 | let mut termlock = self.term_lock.lock(); 517 | termlock.clear_on_exit(clear); 518 | Ok(()) 519 | } 520 | 521 | pub fn draw(&self, draw: &dyn Draw) -> Result<()> { 522 | let mut canvas = TermCanvas { term: &self }; 523 | draw.draw(&mut canvas) 524 | .map_err(|err| TuikitError::DrawError(err)) 525 | } 526 | 527 | pub fn draw_mut(&self, draw: &mut dyn Draw) -> Result<()> { 528 | let mut canvas = TermCanvas { term: &self }; 529 | draw.draw_mut(&mut canvas) 530 | .map_err(|err| TuikitError::DrawError(err)) 531 | } 532 | } 533 | 534 | impl<'a, UserEvent: Send + 'static> Drop for Term { 535 | fn drop(&mut self) { 536 | let _ = self.pause_internal(true); 537 | } 538 | } 539 | 540 | pub struct TermCanvas<'a, UserEvent: Send + 'static> { 541 | term: &'a Term, 542 | } 543 | 544 | impl<'a, UserEvent: Send + 'static> Canvas for TermCanvas<'a, UserEvent> { 545 | fn size(&self) -> Result<(usize, usize)> { 546 | self.term.term_size() 547 | } 548 | 549 | fn clear(&mut self) -> Result<()> { 550 | self.term.clear() 551 | } 552 | 553 | fn put_cell(&mut self, row: usize, col: usize, cell: Cell) -> Result { 554 | self.term.put_cell(row, col, cell) 555 | } 556 | 557 | fn print_with_attr( 558 | &mut self, 559 | row: usize, 560 | col: usize, 561 | content: &str, 562 | attr: Attr, 563 | ) -> Result { 564 | self.term.print_with_attr(row, col, content, attr) 565 | } 566 | 567 | fn set_cursor(&mut self, row: usize, col: usize) -> Result<()> { 568 | self.term.set_cursor(row, col) 569 | } 570 | 571 | fn show_cursor(&mut self, show: bool) -> Result<()> { 572 | self.term.show_cursor(show) 573 | } 574 | } 575 | 576 | struct TermLock { 577 | prefer_height: TermHeight, 578 | max_height: TermHeight, 579 | min_height: TermHeight, 580 | // keep bottom intact when resize? 581 | bottom_intact: bool, 582 | clear_on_exit: bool, 583 | clear_on_start: bool, 584 | mouse_enabled: bool, 585 | alternate_screen: bool, 586 | disable_alternate_screen: bool, 587 | cursor_row: usize, 588 | screen_height: usize, 589 | screen_width: usize, 590 | screen: Screen, 591 | output: Option, 592 | } 593 | 594 | impl Default for TermLock { 595 | fn default() -> Self { 596 | Self { 597 | prefer_height: TermHeight::Percent(100), 598 | max_height: TermHeight::Percent(100), 599 | min_height: TermHeight::Fixed(3), 600 | bottom_intact: false, 601 | alternate_screen: false, 602 | disable_alternate_screen: false, 603 | cursor_row: 0, 604 | screen_height: 0, 605 | screen_width: 0, 606 | screen: Screen::new(0, 0), 607 | output: None, 608 | clear_on_exit: true, 609 | clear_on_start: true, 610 | mouse_enabled: false, 611 | } 612 | } 613 | } 614 | 615 | impl TermLock { 616 | pub fn with_options(options: &TermOptions) -> Self { 617 | let mut term = TermLock::default(); 618 | term.prefer_height = options.height; 619 | term.max_height = options.max_height; 620 | term.min_height = options.min_height; 621 | term.clear_on_exit = options.clear_on_exit; 622 | term.clear_on_start = options.clear_on_start; 623 | term.screen.clear_on_start(options.clear_on_start); 624 | term.disable_alternate_screen = options.disable_alternate_screen; 625 | term.mouse_enabled = options.mouse_enabled; 626 | term 627 | } 628 | 629 | /// Present the content to the terminal 630 | pub fn present(&mut self) -> Result<()> { 631 | let output = self 632 | .output 633 | .as_mut() 634 | .ok_or(TuikitError::TerminalNotStarted)?; 635 | let mut commands = self.screen.present(); 636 | 637 | let cursor_row = self.cursor_row; 638 | // add cursor_row to all CursorGoto commands 639 | for cmd in commands.iter_mut() { 640 | if let Command::CursorGoto { row, col } = *cmd { 641 | *cmd = Command::CursorGoto { 642 | row: row + cursor_row, 643 | col, 644 | } 645 | } 646 | } 647 | 648 | for cmd in commands.into_iter() { 649 | output.execute(cmd); 650 | } 651 | output.flush(); 652 | Ok(()) 653 | } 654 | 655 | /// Resize the internal buffer to according to new terminal size 656 | pub fn on_resize(&mut self) -> Result<()> { 657 | let output = self 658 | .output 659 | .as_mut() 660 | .ok_or(TuikitError::TerminalNotStarted)?; 661 | let (screen_width, screen_height) = output 662 | .terminal_size() 663 | .expect("term:restart get terminal size failed"); 664 | self.screen_height = screen_height; 665 | self.screen_width = screen_width; 666 | 667 | let width = screen_width; 668 | let height = Self::calc_preferred_height( 669 | &self.min_height, 670 | &self.max_height, 671 | &self.prefer_height, 672 | screen_height, 673 | ); 674 | 675 | // update the cursor position 676 | if self.cursor_row + height >= screen_height { 677 | self.bottom_intact = true; 678 | } 679 | 680 | if self.bottom_intact { 681 | self.cursor_row = screen_height - height; 682 | } 683 | 684 | // clear the screen 685 | let _ = output.cursor_goto(self.cursor_row, 0); 686 | if self.clear_on_start { 687 | let _ = output.erase_down(); 688 | } 689 | 690 | // clear the screen buffer 691 | self.screen.resize(width, height); 692 | Ok(()) 693 | } 694 | 695 | fn calc_height(height_spec: &TermHeight, actual_height: usize) -> usize { 696 | match *height_spec { 697 | TermHeight::Fixed(h) => h, 698 | TermHeight::Percent(p) => actual_height * min(p, 100) / 100, 699 | } 700 | } 701 | 702 | fn calc_preferred_height( 703 | min_height: &TermHeight, 704 | max_height: &TermHeight, 705 | prefer_height: &TermHeight, 706 | height: usize, 707 | ) -> usize { 708 | let max_height = Self::calc_height(max_height, height); 709 | let min_height = Self::calc_height(min_height, height); 710 | let prefer_height = Self::calc_height(prefer_height, height); 711 | 712 | // ensure the calculated height is in range (MIN_HEIGHT, height) 713 | let max_height = max(min(max_height, height), MIN_HEIGHT); 714 | let min_height = max(min(min_height, height), MIN_HEIGHT); 715 | max(min(prefer_height, max_height), min_height) 716 | } 717 | 718 | /// Pause the terminal 719 | fn pause(&mut self, exiting: bool) -> Result<()> { 720 | self.disable_mouse()?; 721 | self.output.take().map(|mut output| { 722 | output.show_cursor(); 723 | if self.clear_on_exit || !exiting { 724 | // clear drawn contents 725 | if !self.disable_alternate_screen { 726 | output.quit_alternate_screen(); 727 | } else { 728 | output.cursor_goto(self.cursor_row, 0); 729 | output.erase_down(); 730 | } 731 | } else { 732 | output.cursor_goto(self.cursor_row + self.screen.height(), 0); 733 | if self.bottom_intact { 734 | output.write("\n"); 735 | } 736 | } 737 | output.flush(); 738 | }); 739 | Ok(()) 740 | } 741 | 742 | /// ensure the screen had enough height 743 | /// If the prefer height is full screen, it will enter alternate screen 744 | /// otherwise it will ensure there are enough lines at the bottom 745 | fn ensure_height(&mut self, cursor_pos: (usize, usize)) -> Result<()> { 746 | let output = self 747 | .output 748 | .as_mut() 749 | .ok_or(TuikitError::TerminalNotStarted)?; 750 | 751 | // initialize 752 | 753 | let (screen_width, screen_height) = output 754 | .terminal_size() 755 | .expect("termlock:ensure_height get terminal size failed"); 756 | let height_to_be = Self::calc_preferred_height( 757 | &self.min_height, 758 | &self.max_height, 759 | &self.prefer_height, 760 | screen_height, 761 | ); 762 | 763 | self.alternate_screen = false; 764 | let (mut cursor_row, cursor_col) = cursor_pos; 765 | if height_to_be >= screen_height { 766 | // whole screen 767 | self.alternate_screen = true; 768 | self.bottom_intact = false; 769 | self.cursor_row = 0; 770 | if !self.disable_alternate_screen { 771 | output.enter_alternate_screen(); 772 | } 773 | } else { 774 | // only use part of the screen 775 | 776 | // go to a new line so that existing line won't be messed up 777 | if cursor_col > 0 { 778 | output.write("\n"); 779 | cursor_row += 1; 780 | } 781 | 782 | if (cursor_row + height_to_be) <= screen_height { 783 | self.bottom_intact = false; 784 | self.cursor_row = cursor_row; 785 | } else { 786 | for _ in 0..(height_to_be - 1) { 787 | output.write("\n"); 788 | } 789 | self.bottom_intact = true; 790 | self.cursor_row = min(cursor_row, screen_height - height_to_be); 791 | } 792 | } 793 | 794 | output.cursor_goto(self.cursor_row, 0); 795 | output.flush(); 796 | self.screen_height = screen_height; 797 | self.screen_width = screen_width; 798 | Ok(()) 799 | } 800 | 801 | /// get the start row of the terminal 802 | pub fn get_term_start_row(&self) -> usize { 803 | self.cursor_row 804 | } 805 | 806 | /// restart the terminal 807 | pub fn restart(&mut self, output: Output, cursor_pos: (usize, usize)) -> Result<()> { 808 | // ensure the output area had enough height 809 | self.output.replace(output); 810 | self.ensure_height(cursor_pos)?; 811 | self.on_resize()?; 812 | if self.mouse_enabled { 813 | self.enable_mouse()?; 814 | } 815 | Ok(()) 816 | } 817 | 818 | /// return the printable size(width, height) of the term 819 | pub fn term_size(&self) -> Result<(usize, usize)> { 820 | self.screen.size() 821 | } 822 | 823 | /// clear internal buffer 824 | pub fn clear(&mut self) -> Result<()> { 825 | self.screen.clear() 826 | } 827 | 828 | /// change a cell of position `(row, col)` to `cell` 829 | pub fn put_cell(&mut self, row: usize, col: usize, cell: Cell) -> Result { 830 | self.screen.put_cell(row, col, cell) 831 | } 832 | 833 | /// print `content` starting with position `(row, col)` 834 | pub fn print_with_attr( 835 | &mut self, 836 | row: usize, 837 | col: usize, 838 | content: &str, 839 | attr: impl Into, 840 | ) -> Result { 841 | self.screen.print_with_attr(row, col, content, attr.into()) 842 | } 843 | 844 | /// set cursor position to (row, col) 845 | pub fn set_cursor(&mut self, row: usize, col: usize) -> Result<()> { 846 | self.screen.set_cursor(row, col) 847 | } 848 | 849 | /// show/hide cursor, set `show` to `false` to hide the cursor 850 | pub fn show_cursor(&mut self, show: bool) -> Result<()> { 851 | self.screen.show_cursor(show) 852 | } 853 | 854 | /// Enable mouse support 855 | pub fn enable_mouse_support(&mut self) -> Result<()> { 856 | self.mouse_enabled = true; 857 | self.enable_mouse() 858 | } 859 | 860 | /// Disable mouse support 861 | pub fn disable_mouse_support(&mut self) -> Result<()> { 862 | self.mouse_enabled = false; 863 | self.disable_mouse() 864 | } 865 | 866 | pub fn clear_on_exit(&mut self, clear: bool) { 867 | self.clear_on_exit = clear; 868 | } 869 | 870 | /// Enable mouse (send ANSI codes to enable mouse) 871 | fn enable_mouse(&mut self) -> Result<()> { 872 | let output = self 873 | .output 874 | .as_mut() 875 | .ok_or(TuikitError::TerminalNotStarted)?; 876 | output.enable_mouse_support(); 877 | Ok(()) 878 | } 879 | 880 | /// Disable mouse (send ANSI codes to disable mouse) 881 | fn disable_mouse(&mut self) -> Result<()> { 882 | let output = self 883 | .output 884 | .as_mut() 885 | .ok_or(TuikitError::TerminalNotStarted)?; 886 | output.disable_mouse_support(); 887 | Ok(()) 888 | } 889 | } 890 | 891 | impl Drop for TermLock { 892 | fn drop(&mut self) { 893 | let _ = self.pause(true); 894 | } 895 | } 896 | --------------------------------------------------------------------------------