Simple scripting
This commit is contained in:
commit
7c5794e110
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
379
Cargo.lock
generated
Normal file
379
Cargo.lock
generated
Normal file
|
@ -0,0 +1,379 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "adit"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"rhai",
|
||||
"serde",
|
||||
"termion",
|
||||
"toml",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"const-random",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e"
|
||||
dependencies = [
|
||||
"const-random-macro",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"proc-macro-hack",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.148"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numtoa"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-hack"
|
||||
version = "0.5.20+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_termios"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
|
||||
dependencies = [
|
||||
"redox_syscall 0.1.56",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"redox_syscall 0.2.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai"
|
||||
version = "1.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "637a4f79f65571b1fd1a0ebbae05bbbf58a01faf612abbc3eea15cda34f0b87a"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bitflags 2.4.0",
|
||||
"instant",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"rhai_codegen",
|
||||
"smallvec",
|
||||
"smartstring",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_codegen"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "853977598f084a492323fe2f7896b4100a86284ee8473612de60021ea341310f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.134"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96b3c34c1690edf8174f5b289a336ab03f568a4460d8c6df75f2f3a692b3bc6a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.134"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784ed1fbfa13fe191077537b0d70ec8ad1e903cfe04831da608aa36457cb653d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
|
||||
|
||||
[[package]]
|
||||
name = "smartstring"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"static_assertions",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59bf04c28bee9043ed9ea1e41afc0552288d3aba9c6efdd78903b802926f4879"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termion"
|
||||
version = "1.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8fb22f7cde82c8220e5aeacb3258ed7ce996142c77cba193f203515e26c330"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"numtoa",
|
||||
"redox_syscall 0.1.56",
|
||||
"redox_termios",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1967f4cdfc355b37fd76d2a954fb2ed3871034eb4f26d60537d88795cfc332a9"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "adit"
|
||||
version = "0.1.1"
|
||||
authors = ["able"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
termion = "1"
|
||||
unicode-segmentation = "1"
|
||||
toml = "0.5.8"
|
||||
dirs = "*"
|
||||
rhai = "1.16.1"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "*"
|
||||
features = ["derive"]
|
6
assets/config.rhai
Normal file
6
assets/config.rhai
Normal file
|
@ -0,0 +1,6 @@
|
|||
fn status_bar() {
|
||||
let sb = `${file_name}:${y}:${x} | ${file_type} | ${line_count}`;
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
3
rust-toolchain.toml
Normal file
3
rust-toolchain.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
[toolchain]
|
||||
channel = "nightly"
|
||||
components = ["rust-src", "llvm-tools"]
|
37
shell.nix
Normal file
37
shell.nix
Normal file
|
@ -0,0 +1,37 @@
|
|||
{ pkgs ? import <nixpkgs> { } }:
|
||||
pkgs.mkShell rec {
|
||||
buildInputs = with pkgs; [
|
||||
clang
|
||||
llvmPackages.bintools
|
||||
rustup
|
||||
|
||||
];
|
||||
extraCmds = '''';
|
||||
RUSTC_VERSION = pkgs.lib.readFile ./rust-toolchain.toml;
|
||||
# https://github.com/rust-lang/rust-bindgen#environment-variables
|
||||
LIBCLANG_PATH =
|
||||
pkgs.lib.makeLibraryPath [ pkgs.llvmPackages_latest.libclang.lib ];
|
||||
shellHook = ''
|
||||
export PATH=$PATH:''${CARGO_HOME:-~/.cargo}/bin
|
||||
export PATH=$PATH:''${RUSTUP_HOME:-~/.rustup}/toolchains/$RUSTC_VERSION-x86_64-unknown-linux-gnu/bin/
|
||||
'';
|
||||
# Add precompiled library to rustc search path
|
||||
RUSTFLAGS = (builtins.map (a: "-L ${a}/lib") [
|
||||
# add libraries here (e.g. pkgs.libvmi)
|
||||
]);
|
||||
# Add glibc, clang, glib and other headers to bindgen search path
|
||||
BINDGEN_EXTRA_CLANG_ARGS =
|
||||
# Includes with normal include path
|
||||
(builtins.map (a: ''-I"${a}/include"'') [
|
||||
# add dev libraries here (e.g. pkgs.libvmi.dev)
|
||||
pkgs.glibc.dev
|
||||
])
|
||||
# Includes with special directory paths
|
||||
++ [
|
||||
''
|
||||
-I"${pkgs.llvmPackages_latest.libclang.lib}/lib/clang/${pkgs.llvmPackages_latest.libclang.version}/include"''
|
||||
''-I"${pkgs.glib.dev}/include/glib-2.0"''
|
||||
"-I${pkgs.glib.out}/lib/glib-2.0/include/"
|
||||
];
|
||||
|
||||
}
|
96
src/config.rs
Normal file
96
src/config.rs
Normal file
|
@ -0,0 +1,96 @@
|
|||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::{fs::File, io::Read};
|
||||
|
||||
use crate::Editor;
|
||||
use serde::Deserialize;
|
||||
use termion::color::Rgb;
|
||||
|
||||
use rhai::Dynamic;
|
||||
use rhai::Engine;
|
||||
use rhai::Scope;
|
||||
use rhai::AST;
|
||||
|
||||
pub struct Config<'a> {
|
||||
cfg: String,
|
||||
eng: Engine,
|
||||
pub scope: Scope<'a>,
|
||||
ast: AST,
|
||||
}
|
||||
impl Config<'static> {
|
||||
pub fn new() -> Self {
|
||||
let mut engine = Engine::new();
|
||||
let mut scope = Scope::new();
|
||||
let ast = engine
|
||||
.compile_file(PathBuf::from_str("assets/config.rhai").unwrap())
|
||||
.unwrap();
|
||||
let result = engine.call_fn::<Dynamic>(&mut scope, &ast, "status_bar", ());
|
||||
|
||||
Self {
|
||||
cfg: String::new(),
|
||||
eng: Engine::new(),
|
||||
scope: Scope::new(),
|
||||
ast,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call(&mut self, fn_name: &str) -> String {
|
||||
self.eng
|
||||
.call_fn::<String>(&mut self.scope, &self.ast, fn_name, ())
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct RGB(u8, u8, u8);
|
||||
|
||||
impl RGB {
|
||||
pub fn to_term_color(self) -> Rgb {
|
||||
Rgb(self.0, self.1, self.2)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Theme {
|
||||
pub status_fg_color: RGB,
|
||||
pub status_bg_color: RGB,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn default() -> Self {
|
||||
Self {
|
||||
status_fg_color: RGB(63, 63, 63),
|
||||
status_bg_color: RGB(239, 239, 239),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_config() -> ThemeReturn {
|
||||
match dirs::home_dir() {
|
||||
Some(pathbuf) => {
|
||||
let mut file = File::open(pathbuf);
|
||||
match file {
|
||||
Ok(mut file_handle) => {
|
||||
let mut contents = String::new();
|
||||
match file_handle.read_to_string(&mut contents) {
|
||||
Ok(ma) => {
|
||||
let config: Theme = toml::from_str(&contents).unwrap();
|
||||
|
||||
return ThemeReturn::Theme(config);
|
||||
}
|
||||
|
||||
Err(_) => todo!(),
|
||||
}
|
||||
}
|
||||
Err(_) => todo!(),
|
||||
}
|
||||
}
|
||||
None => ThemeReturn::NoHomeDir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ThemeReturn {
|
||||
Theme(Theme),
|
||||
NoHomeDir,
|
||||
|
||||
GenericError,
|
||||
}
|
170
src/document.rs
Normal file
170
src/document.rs
Normal file
|
@ -0,0 +1,170 @@
|
|||
use crate::FileType;
|
||||
use crate::Position;
|
||||
use crate::Row;
|
||||
use crate::SearchDirection;
|
||||
use std::fs;
|
||||
use std::io::{Error, Write};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Document {
|
||||
pub file_name: Option<String>,
|
||||
rows: Vec<Row>,
|
||||
dirty: bool,
|
||||
file_type: FileType,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
pub fn open(filename: &str) -> Result<Self, std::io::Error> {
|
||||
let contents = fs::read_to_string(filename)?;
|
||||
let file_type = FileType::from(filename);
|
||||
let mut rows = Vec::new();
|
||||
for value in contents.lines() {
|
||||
rows.push(Row::from(value));
|
||||
}
|
||||
Ok(Self {
|
||||
rows,
|
||||
file_name: Some(filename.to_string()),
|
||||
dirty: false,
|
||||
file_type,
|
||||
})
|
||||
}
|
||||
pub fn file_type(&self) -> String {
|
||||
self.file_type.name()
|
||||
}
|
||||
pub fn row(&self, index: usize) -> Option<&Row> {
|
||||
self.rows.get(index)
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.rows.is_empty()
|
||||
}
|
||||
pub fn len(&self) -> usize {
|
||||
self.rows.len()
|
||||
}
|
||||
fn insert_newline(&mut self, at: &Position) {
|
||||
if at.y > self.rows.len() {
|
||||
return;
|
||||
}
|
||||
if at.y == self.rows.len() {
|
||||
self.rows.push(Row::default());
|
||||
return;
|
||||
}
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
let current_row = &mut self.rows[at.y];
|
||||
let new_row = current_row.split(at.x);
|
||||
#[allow(clippy::integer_arithmetic)]
|
||||
self.rows.insert(at.y + 1, new_row);
|
||||
}
|
||||
pub fn insert(&mut self, at: &Position, c: char) {
|
||||
if at.y > self.rows.len() {
|
||||
return;
|
||||
}
|
||||
self.dirty = true;
|
||||
if c == '\n' {
|
||||
self.insert_newline(at);
|
||||
} else if at.y == self.rows.len() {
|
||||
let mut row = Row::default();
|
||||
row.insert(0, c);
|
||||
self.rows.push(row);
|
||||
} else {
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
let row = &mut self.rows[at.y];
|
||||
row.insert(at.x, c);
|
||||
}
|
||||
self.unhighlight_rows(at.y);
|
||||
}
|
||||
|
||||
fn unhighlight_rows(&mut self, start: usize) {
|
||||
let start = start.saturating_sub(1);
|
||||
for row in self.rows.iter_mut().skip(start) {
|
||||
row.is_highlighted = false;
|
||||
}
|
||||
}
|
||||
#[allow(clippy::integer_arithmetic, clippy::indexing_slicing)]
|
||||
pub fn delete(&mut self, at: &Position) {
|
||||
let len = self.rows.len();
|
||||
if at.y >= len {
|
||||
return;
|
||||
}
|
||||
self.dirty = true;
|
||||
if at.x == self.rows[at.y].len() && at.y + 1 < len {
|
||||
let next_row = self.rows.remove(at.y + 1);
|
||||
let row = &mut self.rows[at.y];
|
||||
row.append(&next_row);
|
||||
} else {
|
||||
let row = &mut self.rows[at.y];
|
||||
row.delete(at.x);
|
||||
}
|
||||
self.unhighlight_rows(at.y);
|
||||
}
|
||||
pub fn save(&mut self) -> Result<(), Error> {
|
||||
if let Some(file_name) = &self.file_name {
|
||||
let mut file = fs::File::create(file_name)?;
|
||||
self.file_type = FileType::from(file_name);
|
||||
for row in &mut self.rows {
|
||||
file.write_all(row.as_bytes())?;
|
||||
file.write_all(b"\n")?;
|
||||
}
|
||||
self.dirty = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
self.dirty
|
||||
}
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub fn find(&self, query: &str, at: &Position, direction: SearchDirection) -> Option<Position> {
|
||||
if at.y >= self.rows.len() {
|
||||
return None;
|
||||
}
|
||||
let mut position = Position { x: at.x, y: at.y };
|
||||
|
||||
let start = if direction == SearchDirection::Forward {
|
||||
at.y
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let end = if direction == SearchDirection::Forward {
|
||||
self.rows.len()
|
||||
} else {
|
||||
at.y.saturating_add(1)
|
||||
};
|
||||
for _ in start..end {
|
||||
if let Some(row) = self.rows.get(position.y) {
|
||||
if let Some(x) = row.find(&query, position.x, direction) {
|
||||
position.x = x;
|
||||
return Some(position);
|
||||
}
|
||||
if direction == SearchDirection::Forward {
|
||||
position.y = position.y.saturating_add(1);
|
||||
position.x = 0;
|
||||
} else {
|
||||
position.y = position.y.saturating_sub(1);
|
||||
position.x = self.rows[position.y].len();
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn highlight(&mut self, word: &Option<String>, until: Option<usize>) {
|
||||
let mut start_with_comment = false;
|
||||
let until = if let Some(until) = until {
|
||||
if until.saturating_add(1) < self.rows.len() {
|
||||
until.saturating_add(1)
|
||||
} else {
|
||||
self.rows.len()
|
||||
}
|
||||
} else {
|
||||
self.rows.len()
|
||||
};
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
for row in &mut self.rows[..until] {
|
||||
start_with_comment = row.highlight(
|
||||
&self.file_type.highlighting_options(),
|
||||
word,
|
||||
start_with_comment,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
459
src/editor.rs
Normal file
459
src/editor.rs
Normal file
|
@ -0,0 +1,459 @@
|
|||
use crate::config::Config;
|
||||
use crate::config::Theme;
|
||||
use crate::Document;
|
||||
use crate::Row;
|
||||
use crate::Terminal;
|
||||
use std::env;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use termion::event::Key;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const QUIT_TIMES: u8 = 2;
|
||||
|
||||
#[derive(PartialEq, Copy, Clone)]
|
||||
pub enum SearchDirection {
|
||||
Forward,
|
||||
Backward,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Position {
|
||||
pub x: usize,
|
||||
pub y: usize,
|
||||
}
|
||||
#[derive(Clone)]
|
||||
struct StatusMessage {
|
||||
text: String,
|
||||
time: Instant,
|
||||
}
|
||||
impl StatusMessage {
|
||||
fn from(message: String) -> Self {
|
||||
Self {
|
||||
time: Instant::now(),
|
||||
text: message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Editor<'a> {
|
||||
should_quit: bool,
|
||||
terminal: Terminal,
|
||||
pub cursor_position: Position,
|
||||
offset: Position,
|
||||
pub document: Document,
|
||||
status_message: StatusMessage,
|
||||
quit_times: u8,
|
||||
highlighted_word: Option<String>,
|
||||
pub config: Config<'a>,
|
||||
}
|
||||
|
||||
impl Editor<'static> {
|
||||
pub fn run(&mut self) {
|
||||
loop {
|
||||
if let Err(error) = self.refresh_screen() {
|
||||
die(error);
|
||||
}
|
||||
if self.should_quit {
|
||||
break;
|
||||
}
|
||||
if let Err(error) = self.process_keypress() {
|
||||
die(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn default() -> Self {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let mut initial_status =
|
||||
String::from("HELP: Ctrl-F = find | Ctrl-S = save | Ctrl-Q = quit");
|
||||
|
||||
let document = if let Some(file_name) = args.get(1) {
|
||||
let doc = Document::open(file_name);
|
||||
if let Ok(doc) = doc {
|
||||
doc
|
||||
} else {
|
||||
initial_status = format!("ERR: Could not open file: {}", file_name);
|
||||
Document::default()
|
||||
}
|
||||
} else {
|
||||
Document::default()
|
||||
};
|
||||
|
||||
let mut cfg = Config::new();
|
||||
|
||||
Self {
|
||||
should_quit: false,
|
||||
terminal: Terminal::default().expect("Failed to initialize terminal"),
|
||||
document,
|
||||
cursor_position: Position::default(),
|
||||
offset: Position::default(),
|
||||
status_message: StatusMessage::from(initial_status),
|
||||
quit_times: QUIT_TIMES,
|
||||
highlighted_word: None,
|
||||
config: Config::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_scope(&mut self) {
|
||||
self.config
|
||||
.scope
|
||||
.set_or_push("line_count", self.document.len());
|
||||
|
||||
self.config
|
||||
.scope
|
||||
.set_or_push("file_type", self.document.file_type());
|
||||
|
||||
self.config
|
||||
.scope
|
||||
.set_or_push("x", self.cursor_position.x as u64);
|
||||
self.config
|
||||
.scope
|
||||
.set_or_push("y", self.cursor_position.y as u64);
|
||||
}
|
||||
|
||||
fn refresh_screen(&mut self) -> Result<(), std::io::Error> {
|
||||
self.update_scope();
|
||||
Terminal::cursor_hide();
|
||||
Terminal::cursor_position(&Position::default());
|
||||
if self.should_quit {
|
||||
Terminal::clear_screen();
|
||||
println!("Goodbye.\r");
|
||||
} else {
|
||||
self.document.highlight(
|
||||
&self.highlighted_word,
|
||||
Some(
|
||||
self.offset
|
||||
.y
|
||||
.saturating_add(self.terminal.size().height as usize),
|
||||
),
|
||||
);
|
||||
self.draw_rows();
|
||||
self.draw_status_bar();
|
||||
self.draw_message_bar();
|
||||
Terminal::cursor_position(&Position {
|
||||
x: self.cursor_position.x.saturating_sub(self.offset.x),
|
||||
y: self.cursor_position.y.saturating_sub(self.offset.y),
|
||||
});
|
||||
}
|
||||
Terminal::cursor_show();
|
||||
Terminal::flush()
|
||||
}
|
||||
fn save(&mut self) {
|
||||
if self.document.file_name.is_none() {
|
||||
let new_name = self.prompt("Save as: ", |_, _, _| {}).unwrap_or(None);
|
||||
if new_name.is_none() {
|
||||
self.status_message = StatusMessage::from("Save aborted.".to_string());
|
||||
return;
|
||||
}
|
||||
self.document.file_name = new_name;
|
||||
}
|
||||
|
||||
if self.document.save().is_ok() {
|
||||
self.status_message = StatusMessage::from("File saved successfully.".to_string());
|
||||
} else {
|
||||
self.status_message = StatusMessage::from("Error writing file!".to_string());
|
||||
}
|
||||
}
|
||||
fn search(&mut self) {
|
||||
let old_position = self.cursor_position.clone();
|
||||
let mut direction = SearchDirection::Forward;
|
||||
let query = self
|
||||
.prompt(
|
||||
"Search (ESC to cancel, Arrows to navigate): ",
|
||||
|editor, key, query| {
|
||||
let mut moved = false;
|
||||
match key {
|
||||
Key::Right | Key::Down => {
|
||||
direction = SearchDirection::Forward;
|
||||
editor.move_cursor(Key::Right);
|
||||
moved = true;
|
||||
}
|
||||
Key::Left | Key::Up => direction = SearchDirection::Backward,
|
||||
_ => direction = SearchDirection::Forward,
|
||||
}
|
||||
if let Some(position) =
|
||||
editor
|
||||
.document
|
||||
.find(&query, &editor.cursor_position, direction)
|
||||
{
|
||||
editor.cursor_position = position;
|
||||
editor.scroll();
|
||||
} else if moved {
|
||||
editor.move_cursor(Key::Left);
|
||||
}
|
||||
editor.highlighted_word = Some(query.to_string());
|
||||
},
|
||||
)
|
||||
.unwrap_or(None);
|
||||
|
||||
if query.is_none() {
|
||||
self.cursor_position = old_position;
|
||||
self.scroll();
|
||||
}
|
||||
self.highlighted_word = None;
|
||||
}
|
||||
fn process_keypress(&mut self) -> Result<(), std::io::Error> {
|
||||
let pressed_key = Terminal::read_key()?;
|
||||
match pressed_key {
|
||||
Key::Ctrl('q') => {
|
||||
if self.quit_times > 0 && self.document.is_dirty() {
|
||||
self.status_message = StatusMessage::from(format!(
|
||||
"WARNING! File has unsaved changes. Press Ctrl-Q {} more times to quit.",
|
||||
self.quit_times
|
||||
));
|
||||
self.quit_times -= 1;
|
||||
return Ok(());
|
||||
}
|
||||
self.should_quit = true
|
||||
}
|
||||
Key::Ctrl('s') => self.save(),
|
||||
Key::Ctrl('f') => self.search(),
|
||||
|
||||
// NOTE: I am more of the opinion that adit should use hard tab.
|
||||
Key::Char('\t') => {
|
||||
for c in " ".chars() {
|
||||
self.document.insert(&self.cursor_position, c);
|
||||
self.move_cursor(Key::Right);
|
||||
}
|
||||
}
|
||||
|
||||
Key::Char(c) => {
|
||||
self.document.insert(&self.cursor_position, c);
|
||||
self.move_cursor(Key::Right);
|
||||
}
|
||||
Key::Delete => self.document.delete(&self.cursor_position),
|
||||
Key::Backspace => {
|
||||
if self.cursor_position.x > 0 || self.cursor_position.y > 0 {
|
||||
self.move_cursor(Key::Left);
|
||||
self.document.delete(&self.cursor_position);
|
||||
}
|
||||
}
|
||||
Key::Up
|
||||
| Key::Down
|
||||
| Key::Left
|
||||
| Key::Right
|
||||
| Key::PageUp
|
||||
| Key::PageDown
|
||||
| Key::End
|
||||
| Key::Home => self.move_cursor(pressed_key),
|
||||
_ => (),
|
||||
}
|
||||
self.scroll();
|
||||
if self.quit_times < QUIT_TIMES {
|
||||
self.quit_times = QUIT_TIMES;
|
||||
self.status_message = StatusMessage::from(String::new());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn scroll(&mut self) {
|
||||
let Position { x, y } = self.cursor_position;
|
||||
let width = self.terminal.size().width as usize;
|
||||
let height = self.terminal.size().height as usize;
|
||||
let mut offset = &mut self.offset;
|
||||
if y < offset.y {
|
||||
offset.y = y;
|
||||
} else if y >= offset.y.saturating_add(height) {
|
||||
offset.y = y.saturating_sub(height).saturating_add(1);
|
||||
}
|
||||
if x < offset.x {
|
||||
offset.x = x;
|
||||
} else if x >= offset.x.saturating_add(width) {
|
||||
offset.x = x.saturating_sub(width).saturating_add(1);
|
||||
}
|
||||
}
|
||||
fn move_cursor(&mut self, key: Key) {
|
||||
let terminal_height = self.terminal.size().height as usize;
|
||||
let Position { mut y, mut x } = self.cursor_position;
|
||||
let height = self.document.len();
|
||||
let mut width = if let Some(row) = self.document.row(y) {
|
||||
row.len()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
match key {
|
||||
Key::Up => y = y.saturating_sub(1),
|
||||
Key::Down => {
|
||||
if y < height {
|
||||
y = y.saturating_add(1);
|
||||
}
|
||||
}
|
||||
Key::Left => {
|
||||
if x > 0 {
|
||||
x -= 1;
|
||||
} else if y > 0 {
|
||||
y -= 1;
|
||||
if let Some(row) = self.document.row(y) {
|
||||
x = row.len();
|
||||
} else {
|
||||
x = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Key::Right => {
|
||||
if x < width {
|
||||
x += 1;
|
||||
} else if y < height {
|
||||
y += 1;
|
||||
x = 0;
|
||||
}
|
||||
}
|
||||
Key::PageUp => {
|
||||
y = if y > terminal_height {
|
||||
y.saturating_sub(terminal_height)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
Key::PageDown => {
|
||||
y = if y.saturating_add(terminal_height) < height {
|
||||
y.saturating_add(terminal_height)
|
||||
} else {
|
||||
height
|
||||
}
|
||||
}
|
||||
Key::Home => x = 0,
|
||||
Key::End => x = width,
|
||||
_ => (),
|
||||
}
|
||||
width = if let Some(row) = self.document.row(y) {
|
||||
row.len()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if x > width {
|
||||
x = width;
|
||||
}
|
||||
|
||||
self.cursor_position = Position { x, y }
|
||||
}
|
||||
fn draw_welcome_message(&self) {
|
||||
let mut welcome_message = format!("Able editor -- version {}", VERSION);
|
||||
let width = self.terminal.size().width as usize;
|
||||
let len = welcome_message.len();
|
||||
#[allow(clippy::integer_arithmetic, clippy::integer_division)]
|
||||
let padding = width.saturating_sub(len) / 2;
|
||||
let spaces = " ".repeat(padding.saturating_sub(1));
|
||||
welcome_message = format!("▻{}{}", spaces, welcome_message);
|
||||
welcome_message.truncate(width);
|
||||
println!("{}\r", welcome_message);
|
||||
}
|
||||
pub fn draw_row(&self, row: &Row) {
|
||||
let width = self.terminal.size().width as usize;
|
||||
let start = self.offset.x;
|
||||
let end = self.offset.x.saturating_add(width);
|
||||
let row = row.render(start, end);
|
||||
println!("{}\r", row)
|
||||
}
|
||||
#[allow(clippy::integer_division, clippy::integer_arithmetic)]
|
||||
fn draw_rows(&self) {
|
||||
let height = self.terminal.size().height;
|
||||
for terminal_row in 0..height {
|
||||
Terminal::clear_current_line();
|
||||
if let Some(row) = self
|
||||
.document
|
||||
.row(self.offset.y.saturating_add(terminal_row as usize))
|
||||
{
|
||||
self.draw_row(row);
|
||||
} else if self.document.is_empty() && terminal_row == height / 3 {
|
||||
self.draw_welcome_message();
|
||||
} else {
|
||||
println!("▻\r");
|
||||
}
|
||||
}
|
||||
}
|
||||
fn draw_status_bar(&mut self) {
|
||||
let mut status;
|
||||
let width = self.terminal.size().width as usize;
|
||||
let modified_indicator = if self.document.is_dirty() {
|
||||
" (modified)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let mut file_name = "[No Name]".to_string();
|
||||
if let Some(name) = &self.document.file_name {
|
||||
file_name = name.clone();
|
||||
file_name.truncate(20);
|
||||
}
|
||||
// Make this line scriptable
|
||||
|
||||
status = format!(
|
||||
"{}:{}:{} | {} | {} lines{}",
|
||||
file_name,
|
||||
self.cursor_position.y.saturating_add(1),
|
||||
self.cursor_position.x.saturating_add(1),
|
||||
self.document.file_type(),
|
||||
self.document.len(),
|
||||
modified_indicator,
|
||||
);
|
||||
|
||||
self.config.scope.set_or_push(
|
||||
"file_name",
|
||||
self.document
|
||||
.file_name
|
||||
.clone()
|
||||
.unwrap_or("buffer".to_string()),
|
||||
);
|
||||
{}
|
||||
// status = self.config.call("status_bar");
|
||||
|
||||
#[allow(clippy::integer_arithmetic)]
|
||||
let len = status.len();
|
||||
status.push_str(&" ".repeat(width.saturating_sub(len)));
|
||||
status = format!(" {}", status);
|
||||
status.truncate(width);
|
||||
|
||||
let theme = Theme::default();
|
||||
|
||||
Terminal::set_bg_color(theme.status_bg_color.to_term_color());
|
||||
Terminal::set_fg_color(theme.status_fg_color.to_term_color());
|
||||
println!("{}\r", status);
|
||||
Terminal::reset_fg_color();
|
||||
Terminal::reset_bg_color();
|
||||
}
|
||||
fn draw_message_bar(&self) {
|
||||
Terminal::clear_current_line();
|
||||
let message = &self.status_message;
|
||||
if Instant::now() - message.time < Duration::new(5, 0) {
|
||||
let mut text = message.text.clone();
|
||||
text.truncate(self.terminal.size().width as usize);
|
||||
print!("{}", text);
|
||||
}
|
||||
}
|
||||
fn prompt<C>(&mut self, prompt: &str, mut callback: C) -> Result<Option<String>, std::io::Error>
|
||||
where
|
||||
C: FnMut(&mut Self, Key, &String),
|
||||
{
|
||||
let mut result = String::new();
|
||||
loop {
|
||||
self.status_message = StatusMessage::from(format!("{}{}", prompt, result));
|
||||
self.refresh_screen()?;
|
||||
let key = Terminal::read_key()?;
|
||||
match key {
|
||||
Key::Backspace => result.truncate(result.len().saturating_sub(1)),
|
||||
Key::Char('\n') => break,
|
||||
Key::Char(c) => {
|
||||
if !c.is_control() {
|
||||
result.push(c);
|
||||
}
|
||||
}
|
||||
Key::Esc => {
|
||||
result.truncate(0);
|
||||
break;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
callback(self, key, &result);
|
||||
}
|
||||
self.status_message = StatusMessage::from(String::new());
|
||||
if result.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(result))
|
||||
}
|
||||
}
|
||||
|
||||
fn die(e: std::io::Error) {
|
||||
Terminal::clear_screen();
|
||||
panic!("{}", e);
|
||||
}
|
142
src/filetype.rs
Normal file
142
src/filetype.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
pub struct FileType {
|
||||
name: String,
|
||||
hl_opts: HighlightingOptions,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HighlightingOptions {
|
||||
numbers: bool,
|
||||
strings: bool,
|
||||
characters: bool,
|
||||
// TODO make this configurable
|
||||
comments: bool,
|
||||
multiline_comments: bool,
|
||||
primary_keywords: Vec<String>,
|
||||
secondary_keywords: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for FileType {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: String::from("No filetype"),
|
||||
hl_opts: HighlightingOptions::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
pub fn name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
pub fn highlighting_options(&self) -> &HighlightingOptions {
|
||||
&self.hl_opts
|
||||
}
|
||||
pub fn from(file_name: &str) -> Self {
|
||||
if file_name.ends_with(".rs") {
|
||||
return Self {
|
||||
name: String::from("Rust"),
|
||||
hl_opts: HighlightingOptions {
|
||||
numbers: true,
|
||||
strings: true,
|
||||
characters: true,
|
||||
comments: true,
|
||||
multiline_comments: true,
|
||||
primary_keywords: vec![
|
||||
"as".to_string(),
|
||||
"break".to_string(),
|
||||
"const".to_string(),
|
||||
"continue".to_string(),
|
||||
"crate".to_string(),
|
||||
"else".to_string(),
|
||||
"enum".to_string(),
|
||||
"extern".to_string(),
|
||||
"false".to_string(),
|
||||
"fn".to_string(),
|
||||
"for".to_string(),
|
||||
"if".to_string(),
|
||||
"impl".to_string(),
|
||||
"in".to_string(),
|
||||
"let".to_string(),
|
||||
"loop".to_string(),
|
||||
"match".to_string(),
|
||||
"mod".to_string(),
|
||||
"move".to_string(),
|
||||
"mut".to_string(),
|
||||
"pub".to_string(),
|
||||
"ref".to_string(),
|
||||
"return".to_string(),
|
||||
"self".to_string(),
|
||||
"Self".to_string(),
|
||||
"static".to_string(),
|
||||
"struct".to_string(),
|
||||
"super".to_string(),
|
||||
"trait".to_string(),
|
||||
"true".to_string(),
|
||||
"type".to_string(),
|
||||
"unsafe".to_string(),
|
||||
"use".to_string(),
|
||||
"where".to_string(),
|
||||
"while".to_string(),
|
||||
"dyn".to_string(),
|
||||
"abstract".to_string(),
|
||||
"become".to_string(),
|
||||
"box".to_string(),
|
||||
"do".to_string(),
|
||||
"final".to_string(),
|
||||
"macro".to_string(),
|
||||
"override".to_string(),
|
||||
"priv".to_string(),
|
||||
"typeof".to_string(),
|
||||
"unsized".to_string(),
|
||||
"virtual".to_string(),
|
||||
"yield".to_string(),
|
||||
"async".to_string(),
|
||||
"await".to_string(),
|
||||
"try".to_string(),
|
||||
],
|
||||
secondary_keywords: vec![
|
||||
"bool".to_string(),
|
||||
"char".to_string(),
|
||||
"i8".to_string(),
|
||||
"i16".to_string(),
|
||||
"i32".to_string(),
|
||||
"i64".to_string(),
|
||||
"isize".to_string(),
|
||||
"u8".to_string(),
|
||||
"u16".to_string(),
|
||||
"u32".to_string(),
|
||||
"u64".to_string(),
|
||||
"usize".to_string(),
|
||||
"f32".to_string(),
|
||||
"f64".to_string(),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl HighlightingOptions {
|
||||
pub fn numbers(&self) -> bool {
|
||||
self.numbers
|
||||
}
|
||||
pub fn strings(&self) -> bool {
|
||||
self.strings
|
||||
}
|
||||
pub fn characters(&self) -> bool {
|
||||
self.characters
|
||||
}
|
||||
pub fn comments(&self) -> bool {
|
||||
self.comments
|
||||
}
|
||||
pub fn primary_keywords(&self) -> &Vec<String> {
|
||||
&self.primary_keywords
|
||||
}
|
||||
pub fn secondary_keywords(&self) -> &Vec<String> {
|
||||
&self.secondary_keywords
|
||||
}
|
||||
pub fn multiline_comments(&self) -> bool {
|
||||
self.multiline_comments
|
||||
}
|
||||
}
|
28
src/highlighting.rs
Normal file
28
src/highlighting.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use termion::color;
|
||||
#[derive(PartialEq, Clone, Copy, Debug)]
|
||||
pub enum Type {
|
||||
None,
|
||||
Number,
|
||||
Match,
|
||||
String,
|
||||
Character,
|
||||
Comment,
|
||||
MultilineComment,
|
||||
PrimaryKeywords,
|
||||
SecondaryKeywords,
|
||||
}
|
||||
|
||||
impl Type {
|
||||
pub fn to_color(self) -> impl color::Color {
|
||||
match self {
|
||||
Type::Number => color::Rgb(220, 163, 163),
|
||||
Type::Match => color::Rgb(38, 139, 210),
|
||||
Type::String => color::Rgb(211, 54, 130),
|
||||
Type::Character => color::Rgb(108, 113, 196),
|
||||
Type::Comment | Type::MultilineComment => color::Rgb(133, 153, 0),
|
||||
Type::PrimaryKeywords => color::Rgb(181, 137, 0),
|
||||
Type::SecondaryKeywords => color::Rgb(42, 161, 152),
|
||||
_ => color::Rgb(255, 255, 255),
|
||||
}
|
||||
}
|
||||
}
|
29
src/main.rs
Normal file
29
src/main.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
#![warn(clippy::all, clippy::pedantic, clippy::restriction)]
|
||||
#![allow(
|
||||
clippy::missing_docs_in_private_items,
|
||||
clippy::implicit_return,
|
||||
clippy::shadow_reuse,
|
||||
clippy::print_stdout,
|
||||
clippy::wildcard_enum_match_arm,
|
||||
clippy::else_if_without_else
|
||||
)]
|
||||
mod config;
|
||||
mod document;
|
||||
mod editor;
|
||||
mod filetype;
|
||||
mod highlighting;
|
||||
mod row;
|
||||
mod terminal;
|
||||
|
||||
pub use document::Document;
|
||||
use editor::Editor;
|
||||
pub use editor::Position;
|
||||
pub use editor::SearchDirection;
|
||||
pub use filetype::FileType;
|
||||
pub use filetype::HighlightingOptions;
|
||||
pub use row::Row;
|
||||
pub use terminal::Terminal;
|
||||
|
||||
fn main() {
|
||||
Editor::default().run();
|
||||
}
|
513
src/row.rs
Normal file
513
src/row.rs
Normal file
|
@ -0,0 +1,513 @@
|
|||
use crate::highlighting;
|
||||
use crate::HighlightingOptions;
|
||||
use crate::SearchDirection;
|
||||
use std::cmp;
|
||||
use termion::color;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Row {
|
||||
string: String,
|
||||
highlighting: Vec<highlighting::Type>,
|
||||
pub is_highlighted: bool,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl From<&str> for Row {
|
||||
fn from(slice: &str) -> Self {
|
||||
Self {
|
||||
string: String::from(slice),
|
||||
highlighting: Vec::new(),
|
||||
is_highlighted: false,
|
||||
len: slice.graphemes(true).count(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Row {
|
||||
pub fn render(&self, start: usize, end: usize) -> String {
|
||||
let end = cmp::min(end, self.string.len());
|
||||
let start = cmp::min(start, end);
|
||||
let mut result = String::new();
|
||||
let mut current_highlighting = &highlighting::Type::None;
|
||||
#[allow(clippy::integer_arithmetic)]
|
||||
for (index, grapheme) in self.string[..]
|
||||
.graphemes(true)
|
||||
.enumerate()
|
||||
.skip(start)
|
||||
.take(end - start)
|
||||
{
|
||||
if let Some(c) = grapheme.chars().next() {
|
||||
let highlighting_type = self
|
||||
.highlighting
|
||||
.get(index)
|
||||
.unwrap_or(&highlighting::Type::None);
|
||||
if highlighting_type != current_highlighting {
|
||||
current_highlighting = highlighting_type;
|
||||
let start_highlight =
|
||||
format!("{}", termion::color::Fg(highlighting_type.to_color()));
|
||||
result.push_str(&start_highlight[..]);
|
||||
}
|
||||
if c == '\t' {
|
||||
// TODO: Make this configurable
|
||||
result.push_str(" ");
|
||||
} else {
|
||||
result.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
let end_highlight = format!("{}", termion::color::Fg(color::Reset));
|
||||
result.push_str(&end_highlight[..]);
|
||||
result
|
||||
}
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len == 0
|
||||
}
|
||||
pub fn insert(&mut self, at: usize, c: char) {
|
||||
if at >= self.len() {
|
||||
self.string.push(c);
|
||||
self.len += 1;
|
||||
return;
|
||||
}
|
||||
let mut result: String = String::new();
|
||||
let mut length = 0;
|
||||
for (index, grapheme) in self.string[..].graphemes(true).enumerate() {
|
||||
length += 1;
|
||||
if index == at {
|
||||
length += 1;
|
||||
result.push(c);
|
||||
}
|
||||
result.push_str(grapheme);
|
||||
}
|
||||
self.len = length;
|
||||
self.string = result;
|
||||
}
|
||||
pub fn delete(&mut self, at: usize) {
|
||||
if at >= self.len() {
|
||||
return;
|
||||
}
|
||||
let mut result: String = String::new();
|
||||
let mut length = 0;
|
||||
for (index, grapheme) in self.string[..].graphemes(true).enumerate() {
|
||||
if index != at {
|
||||
length += 1;
|
||||
result.push_str(grapheme);
|
||||
}
|
||||
}
|
||||
self.len = length;
|
||||
self.string = result;
|
||||
}
|
||||
pub fn append(&mut self, new: &Self) {
|
||||
self.string = format!("{}{}", self.string, new.string);
|
||||
self.len += new.len;
|
||||
}
|
||||
pub fn split(&mut self, at: usize) -> Self {
|
||||
let mut row: String = String::new();
|
||||
let mut length = 0;
|
||||
let mut splitted_row: String = String::new();
|
||||
let mut splitted_length = 0;
|
||||
for (index, grapheme) in self.string[..].graphemes(true).enumerate() {
|
||||
if index < at {
|
||||
length += 1;
|
||||
row.push_str(grapheme);
|
||||
} else {
|
||||
splitted_length += 1;
|
||||
splitted_row.push_str(grapheme);
|
||||
}
|
||||
}
|
||||
|
||||
self.string = row;
|
||||
self.len = length;
|
||||
self.is_highlighted = false;
|
||||
Self {
|
||||
string: splitted_row,
|
||||
len: splitted_length,
|
||||
is_highlighted: false,
|
||||
highlighting: Vec::new(),
|
||||
}
|
||||
}
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.string.as_bytes()
|
||||
}
|
||||
pub fn find(&self, query: &str, at: usize, direction: SearchDirection) -> Option<usize> {
|
||||
if at > self.len || query.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let start = if direction == SearchDirection::Forward {
|
||||
at
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let end = if direction == SearchDirection::Forward {
|
||||
self.len
|
||||
} else {
|
||||
at
|
||||
};
|
||||
#[allow(clippy::integer_arithmetic)]
|
||||
let substring: String = self.string[..]
|
||||
.graphemes(true)
|
||||
.skip(start)
|
||||
.take(end - start)
|
||||
.collect();
|
||||
let matching_byte_index = if direction == SearchDirection::Forward {
|
||||
substring.find(query)
|
||||
} else {
|
||||
substring.rfind(query)
|
||||
};
|
||||
if let Some(matching_byte_index) = matching_byte_index {
|
||||
for (grapheme_index, (byte_index, _)) in
|
||||
substring[..].grapheme_indices(true).enumerate()
|
||||
{
|
||||
if matching_byte_index == byte_index {
|
||||
#[allow(clippy::integer_arithmetic)]
|
||||
return Some(start + grapheme_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn highlight_match(&mut self, word: &Option<String>) {
|
||||
if let Some(word) = word {
|
||||
if word.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut index = 0;
|
||||
while let Some(search_match) = self.find(word, index, SearchDirection::Forward) {
|
||||
if let Some(next_index) = search_match.checked_add(word[..].graphemes(true).count())
|
||||
{
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
for i in search_match..next_index {
|
||||
self.highlighting[i] = highlighting::Type::Match;
|
||||
}
|
||||
index = next_index;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn highlight_str(
|
||||
&mut self,
|
||||
index: &mut usize,
|
||||
substring: &str,
|
||||
chars: &[char],
|
||||
hl_type: highlighting::Type,
|
||||
) -> bool {
|
||||
if substring.is_empty() {
|
||||
return false;
|
||||
}
|
||||
for (substring_index, c) in substring.chars().enumerate() {
|
||||
if let Some(next_char) = chars.get(index.saturating_add(substring_index)) {
|
||||
if *next_char != c {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for _ in 0..substring.len() {
|
||||
self.highlighting.push(hl_type);
|
||||
*index += 1;
|
||||
}
|
||||
true
|
||||
}
|
||||
fn highlight_keywords(
|
||||
&mut self,
|
||||
index: &mut usize,
|
||||
chars: &[char],
|
||||
keywords: &[String],
|
||||
hl_type: highlighting::Type,
|
||||
) -> bool {
|
||||
if *index > 0 {
|
||||
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
|
||||
let prev_char = chars[*index - 1];
|
||||
if !is_separator(prev_char) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for word in keywords {
|
||||
if *index < chars.len().saturating_sub(word.len()) {
|
||||
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
|
||||
let next_char = chars[*index + word.len()];
|
||||
if !is_separator(next_char) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if self.highlight_str(index, &word, chars, hl_type) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn highlight_primary_keywords(
|
||||
&mut self,
|
||||
index: &mut usize,
|
||||
opts: &HighlightingOptions,
|
||||
chars: &[char],
|
||||
) -> bool {
|
||||
self.highlight_keywords(
|
||||
index,
|
||||
chars,
|
||||
opts.primary_keywords(),
|
||||
highlighting::Type::PrimaryKeywords,
|
||||
)
|
||||
}
|
||||
fn highlight_secondary_keywords(
|
||||
&mut self,
|
||||
index: &mut usize,
|
||||
opts: &HighlightingOptions,
|
||||
chars: &[char],
|
||||
) -> bool {
|
||||
self.highlight_keywords(
|
||||
index,
|
||||
chars,
|
||||
opts.secondary_keywords(),
|
||||
highlighting::Type::SecondaryKeywords,
|
||||
)
|
||||
}
|
||||
|
||||
fn highlight_char(
|
||||
&mut self,
|
||||
index: &mut usize,
|
||||
opts: &HighlightingOptions,
|
||||
c: char,
|
||||
chars: &[char],
|
||||
) -> bool {
|
||||
if opts.characters() && c == '\'' {
|
||||
if let Some(next_char) = chars.get(index.saturating_add(1)) {
|
||||
let closing_index = if *next_char == '\\' {
|
||||
index.saturating_add(3)
|
||||
} else {
|
||||
index.saturating_add(2)
|
||||
};
|
||||
if let Some(closing_char) = chars.get(closing_index) {
|
||||
if *closing_char == '\'' {
|
||||
for _ in 0..=closing_index.saturating_sub(*index) {
|
||||
self.highlighting.push(highlighting::Type::Character);
|
||||
*index += 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn highlight_comment(
|
||||
&mut self,
|
||||
index: &mut usize,
|
||||
opts: &HighlightingOptions,
|
||||
c: char,
|
||||
chars: &[char],
|
||||
) -> bool {
|
||||
if opts.comments() && c == '/' && *index < chars.len() {
|
||||
if let Some(next_char) = chars.get(index.saturating_add(1)) {
|
||||
if *next_char == '/' {
|
||||
for _ in *index..chars.len() {
|
||||
self.highlighting.push(highlighting::Type::Comment);
|
||||
*index += 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
false
|
||||
}
|
||||
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
|
||||
fn highlight_multiline_comment(
|
||||
&mut self,
|
||||
index: &mut usize,
|
||||
opts: &HighlightingOptions,
|
||||
c: char,
|
||||
chars: &[char],
|
||||
) -> bool {
|
||||
if opts.comments() && c == '/' && *index < chars.len() {
|
||||
if let Some(next_char) = chars.get(index.saturating_add(1)) {
|
||||
if *next_char == '*' {
|
||||
let closing_index =
|
||||
if let Some(closing_index) = self.string[*index + 2..].find("*/") {
|
||||
*index + closing_index + 4
|
||||
} else {
|
||||
chars.len()
|
||||
};
|
||||
for _ in *index..closing_index {
|
||||
self.highlighting.push(highlighting::Type::MultilineComment);
|
||||
*index += 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn highlight_string(
|
||||
&mut self,
|
||||
index: &mut usize,
|
||||
opts: &HighlightingOptions,
|
||||
c: char,
|
||||
chars: &[char],
|
||||
) -> bool {
|
||||
if opts.strings() && c == '"' {
|
||||
loop {
|
||||
self.highlighting.push(highlighting::Type::String);
|
||||
*index += 1;
|
||||
if let Some(next_char) = chars.get(*index) {
|
||||
if *next_char == '"' {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.highlighting.push(highlighting::Type::String);
|
||||
*index += 1;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
fn highlight_number(
|
||||
&mut self,
|
||||
index: &mut usize,
|
||||
opts: &HighlightingOptions,
|
||||
c: char,
|
||||
chars: &[char],
|
||||
) -> bool {
|
||||
if opts.numbers() && c.is_ascii_digit() {
|
||||
if *index > 0 {
|
||||
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
|
||||
let prev_char = chars[*index - 1];
|
||||
if !is_separator(prev_char) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
loop {
|
||||
self.highlighting.push(highlighting::Type::Number);
|
||||
*index += 1;
|
||||
if let Some(next_char) = chars.get(*index) {
|
||||
if *next_char != '.' && !next_char.is_ascii_digit() {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
|
||||
pub fn highlight(
|
||||
&mut self,
|
||||
opts: &HighlightingOptions,
|
||||
word: &Option<String>,
|
||||
start_with_comment: bool,
|
||||
) -> bool {
|
||||
let chars: Vec<char> = self.string.chars().collect();
|
||||
if self.is_highlighted && word.is_none() {
|
||||
if let Some(hl_type) = self.highlighting.last() {
|
||||
if *hl_type == highlighting::Type::MultilineComment
|
||||
&& self.string.len() > 1
|
||||
&& self.string[self.string.len() - 2..] == *"*/"
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
self.highlighting = Vec::new();
|
||||
let mut index = 0;
|
||||
let mut in_ml_comment = start_with_comment;
|
||||
if in_ml_comment {
|
||||
let closing_index = if let Some(closing_index) = self.string.find("*/") {
|
||||
closing_index + 2
|
||||
} else {
|
||||
chars.len()
|
||||
};
|
||||
for _ in 0..closing_index {
|
||||
self.highlighting.push(highlighting::Type::MultilineComment);
|
||||
}
|
||||
index = closing_index;
|
||||
}
|
||||
while let Some(c) = chars.get(index) {
|
||||
if self.highlight_multiline_comment(&mut index, &opts, *c, &chars) {
|
||||
in_ml_comment = true;
|
||||
continue;
|
||||
}
|
||||
in_ml_comment = false;
|
||||
if self.highlight_char(&mut index, opts, *c, &chars)
|
||||
|| self.highlight_comment(&mut index, opts, *c, &chars)
|
||||
|| self.highlight_primary_keywords(&mut index, &opts, &chars)
|
||||
|| self.highlight_secondary_keywords(&mut index, &opts, &chars)
|
||||
|| self.highlight_string(&mut index, opts, *c, &chars)
|
||||
|| self.highlight_number(&mut index, opts, *c, &chars)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
self.highlighting.push(highlighting::Type::None);
|
||||
index += 1;
|
||||
}
|
||||
self.highlight_match(word);
|
||||
if in_ml_comment && &self.string[self.string.len().saturating_sub(2)..] != "*/" {
|
||||
return true;
|
||||
}
|
||||
self.is_highlighted = true;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn is_separator(c: char) -> bool {
|
||||
c.is_ascii_punctuation() || c.is_ascii_whitespace()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_super {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_highlight_find() {
|
||||
let mut row = Row::from("1testtest");
|
||||
row.highlighting = vec![
|
||||
highlighting::Type::Number,
|
||||
highlighting::Type::None,
|
||||
highlighting::Type::None,
|
||||
highlighting::Type::None,
|
||||
highlighting::Type::None,
|
||||
highlighting::Type::None,
|
||||
highlighting::Type::None,
|
||||
highlighting::Type::None,
|
||||
highlighting::Type::None,
|
||||
];
|
||||
row.highlight_match(&Some("t".to_string()));
|
||||
assert_eq!(
|
||||
vec![
|
||||
highlighting::Type::Number,
|
||||
highlighting::Type::Match,
|
||||
highlighting::Type::None,
|
||||
highlighting::Type::None,
|
||||
highlighting::Type::Match,
|
||||
highlighting::Type::Match,
|
||||
highlighting::Type::None,
|
||||
highlighting::Type::None,
|
||||
highlighting::Type::Match
|
||||
],
|
||||
row.highlighting
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find() {
|
||||
let row = Row::from("1testtest");
|
||||
assert_eq!(row.find("t", 0, SearchDirection::Forward), Some(1));
|
||||
assert_eq!(row.find("t", 2, SearchDirection::Forward), Some(4));
|
||||
assert_eq!(row.find("t", 5, SearchDirection::Forward), Some(5));
|
||||
}
|
||||
}
|
77
src/terminal.rs
Normal file
77
src/terminal.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use crate::Position;
|
||||
use std::io::{self, stdout, Write};
|
||||
use termion::color;
|
||||
use termion::event::Key;
|
||||
use termion::input::TermRead;
|
||||
use termion::raw::{IntoRawMode, RawTerminal};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Size {
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
pub struct Terminal {
|
||||
size: Size,
|
||||
_stdout: RawTerminal<std::io::Stdout>,
|
||||
}
|
||||
|
||||
impl Terminal {
|
||||
pub fn default() -> Result<Self, std::io::Error> {
|
||||
let size = termion::terminal_size()?;
|
||||
Ok(Self {
|
||||
size: Size {
|
||||
width: size.0,
|
||||
height: size.1.saturating_sub(2),
|
||||
},
|
||||
_stdout: stdout().into_raw_mode()?,
|
||||
})
|
||||
}
|
||||
pub fn size(&self) -> &Size {
|
||||
&self.size
|
||||
}
|
||||
pub fn clear_screen() {
|
||||
print!("{}", termion::clear::All);
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub fn cursor_position(position: &Position) {
|
||||
let Position { mut x, mut y } = position;
|
||||
x = x.saturating_add(1);
|
||||
y = y.saturating_add(1);
|
||||
let x = x as u16;
|
||||
let y = y as u16;
|
||||
print!("{}", termion::cursor::Goto(x, y));
|
||||
}
|
||||
pub fn flush() -> Result<(), std::io::Error> {
|
||||
io::stdout().flush()
|
||||
}
|
||||
pub fn read_key() -> Result<Key, std::io::Error> {
|
||||
loop {
|
||||
if let Some(key) = io::stdin().lock().keys().next() {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn cursor_hide() {
|
||||
print!("{}", termion::cursor::Hide);
|
||||
}
|
||||
pub fn cursor_show() {
|
||||
print!("{}", termion::cursor::Show);
|
||||
}
|
||||
pub fn clear_current_line() {
|
||||
print!("{}", termion::clear::CurrentLine);
|
||||
}
|
||||
pub fn set_bg_color(color: color::Rgb) {
|
||||
print!("{}", color::Bg(color));
|
||||
}
|
||||
pub fn reset_bg_color() {
|
||||
print!("{}", color::Bg(color::Reset));
|
||||
}
|
||||
pub fn set_fg_color(color: color::Rgb) {
|
||||
print!("{}", color::Fg(color));
|
||||
}
|
||||
pub fn reset_fg_color() {
|
||||
print!("{}", color::Fg(color::Reset));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue