Repository: ilai-deutel/kibi (language: rust)

Issue:
Make kibi compatible with Windows 10
Hi,
I am trying to build kibi for Windows 10 but it fails with compilation errors.
Can you please help.

### Rust Info
- cargo 1.43.0-nightly (3c53211c3 2020-02-07)
- rustc 1.43.0-nightly (e620d0f33 2020-02-18)

### Windows Info
OS Name:                   Microsoft Windows 10 Enterprise
OS Version:                10.0.19041 N/A Build 19041
OS Manufacturer:           Microsoft Corporation
OS Configuration:          Standalone Workstation

### Compiler output
```
$ cargo build --release --locked --all-features

   Compiling kibi v0.1.2 (C:\Users\vanand\kibi\kibi)
error[E0433]: failed to resolve: could not find `sys` in `nix`
 --> src\editor.rs:6:10
  |
6 | use nix::sys::termios::Termios;
  |          ^^^ could not find `sys` in `nix`

error[E0432]: unresolved imports `signal_hook::iterator`, `signal_hook::SIGWINCH`
 --> src\editor.rs:7:19
  |
7 | use signal_hook::{iterator::Signals, SIGWINCH};
  |                   ^^^^^^^^           ^^^^^^^^ no `SIGWINCH` in the root
  |                   |
  |                   could not find `iterator` in `signal_hook`

error[E0432]: unresolved imports `libc::STDIN_FILENO`, `libc::STDOUT_FILENO`, `libc::TIOCGWINSZ`, `libc::VMIN`, `libc::VTIME`
 --> src\terminal.rs:3:12
  |
3 | use libc::{STDIN_FILENO, STDOUT_FILENO, TIOCGWINSZ, VMIN, VTIME};
  |            ^^^^^^^^^^^^  ^^^^^^^^^^^^^  ^^^^^^^^^^  ^^^^  ^^^^^
  |            |             |              |           |     |
  |            |             |              |           |     no `VTIME` in the root
  |            |             |              |           |     help: a similar name exists in the module: `ETIME`
  |            |             |              |           no `VMIN` in the root
  |            |             |              no `TIOCGWINSZ` in the root
  |            |             no `STDOUT_FILENO` in the root
  |            no `STDIN_FILENO` in the root

error[E0432]: unresolved imports `nix::pty`, `nix::sys`
 --> src\terminal.rs:4:11
  |
4 | use nix::{pty::Winsize, sys::termios};
  |           ^^^           ^^^ could not find `sys` in `nix`
  |           |
  |           could not find `pty` in `nix`

error[E0433]: failed to resolve: could not find `ioctl_read_bad` in `nix`
  --> src\terminal.rs:35:10
   |
35 |     nix::ioctl_read_bad!(get_ws, TIOCGWINSZ, Winsize);
   |          ^^^^^^^^^^^^^^ could not find `ioctl_read_bad` in `nix`

error[E0412]: cannot find type `Termios` in this scope
   --> src\editor.rs:109:19
    |
109 |     orig_termios: Termios,
    |                   ^^^^^^^ not found in this scope

error[E0412]: cannot find type `Error` in crate `nix`
 --> src\error.rs:8:14
  |
8 |     Nix(nix::Error),
  |              ^^^^^ not found in `nix`
  |
help: possible candidates are found in other modules, you can import them into scope
  |
3 | use core::fmt::Error;
  |
3 | use crate::error::Error;
  |
3 | use std::error::Error;
  |
3 | use std::fmt::Error;
  |
    and 1 other candidate

error[E0412]: cannot find type `Error` in crate `nix`
  --> src\error.rs:33:16
   |
33 | impl From<nix::Error> for Error {
   |                ^^^^^ not found in `nix`
   |
help: possible candidates are found in other modules, you can import them into scope
   |
3  | use core::fmt::Error;
   |
3  | use crate::error::Error;
   |
3  | use std::error::Error;
   |
3  | use std::fmt::Error;
   |
     and 1 other candidate

error[E0412]: cannot find type `Error` in crate `nix`
  --> src\error.rs:35:23
   |
35 |     fn from(err: nix::Error) -> Self { Self::Nix(err) }
   |                       ^^^^^ not found in `nix`
   |
help: possible candidates are found in other modules, you can import them into scope
   |
3  | use core::fmt::Error;
   |
3  | use crate::error::Error;
   |
3  | use std::error::Error;
   |
3  | use std::fmt::Error;
   |
     and 1 other candidate

error[E0412]: cannot find type `Error` in crate `nix`
 --> src\terminal.rs:8:71
  |
8 | pub(crate) fn set_termios(term: &termios::Termios) -> Result<(), nix::Error> {
  |                                                                       ^^^^^ not found in `nix`
  |
help: possible candidates are found in other modules, you can import them into scope
  |
1 | use core::fmt::Error;
  |
1 | use crate::error::Error;
  |
1 | use std::error::Error;
  |
1 | use std::fmt::Error;
  |
    and 1 other candidate

error[E0425]: cannot find function `get_ws` in this scope
  --> src\terminal.rs:43:14
   |
43 |     unsafe { get_ws(STDOUT_FILENO, maybe_ws.as_mut_ptr()).ok().map(|_| maybe_ws.assume_init()) }
   |              ^^^^^^ not found in this scope

error: aborting due to 11 previous errors

```


Discussion / hints:
Hi there,

Currently, kibi is not compatible with Windows because system calls specific to *nix platform (Linux, MacOS, ...) are made: 

1. POSIX terminal library calls ([`termios`](https://linux.die.net/man/3/termios)) to enable and disable terminal raw mode (`enable_raw_mode()`)
2. Obtaining the terminal size (`get_window_size()`):
    - via [`ioctl`](http://man.cat-v.org/unix_7th/2/ioctl)
    - using ANSI escape sequences if the `ioctl` method fails
3. Signal handling for [`SIGWINCH`](https://www.gnu.org/software/libc/manual/html_node/Miscellaneous-Signals.html)  to know when the terminal has been resized

**Windows 10**

Enabling / disabling raw mode (1.) is necessary to be able to use a text editor. I think one of the crates that safely wrap [`winapi::um::consoleapi::SetConsoleMode`](https://docs.rs/winapi/0.3.8/winapi/um/consoleapi/fn.SetConsoleMode.html) could be used.

Obtaining the terminal size (2.) could potentially work using ANSI escape sequences with recent versions of Windows 10 that support them (since version 1511, released in November 2015). This method is currently implemented in kibi as [`get_window_size_v2`](https://github.com/ilai-deutel/kibi/blob/bf5d98be1988c55408739af9b3eb5c604d7d1e69/src/terminal.rs#L40).

Automatic resize of the terminal window (3.) is nice, but it could be disabled for Windows until an alternative is implemented.

**Windows 7**

If I understand correctly, Windows 7 cannot handle ANSI escape sequences, so I'm not sure if it could work without a major change.

Fix patch:
diff --git a/.travis.yml b/.travis.yml
index 0c740898..46326f5b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -32,7 +32,7 @@ jobs:
       install: skip
       script:
         - cargo test --test loc
-        - cargo clippy --all-targets -- -D warnings
+        - cargo clippy --all-targets -- -D warnings -W pedantic
     - name: Rustfmt
       # Install the rustfmt component for the nightly channel
       # Sometimes, not all components are available in any given nightly, which causes errors such as
diff --git a/Cargo.lock b/Cargo.lock
index 32f57230..ecb305c9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -434,6 +434,8 @@ dependencies = [
  "signal-hook",
  "tokei",
  "unicode-width",
+ "winapi 0.3.8",
+ "winapi-util",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 6c19fb9c..51125ccc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,13 +9,19 @@ repository = "https://github.com/ilai-deutel/kibi"
 readme = "README.md"
 keywords = ["editor", "terminal", "text-editor"]
 categories = ["text-editors", "development-tools"]
-exclude = [".gitignore", "rustfmt.toml", "syntax"]
+include = ["src/**/*", "Cargo.toml"]
 
 [dependencies]
+unicode-width = "0.1.7"
+
+[target.'cfg(unix)'.dependencies]
 libc = "0.2.66"
 nix = "0.17.0"
 signal-hook = "0.1.13"
-unicode-width = "0.1.7"
+
+[target.'cfg(windows)'.dependencies]
+winapi = { version = "0.3.8", default-features = false, features = ["wincon"] }
+winapi-util = "0.1.3"
 
 [dev-dependencies]
 tokei = "10.1.1"
diff --git a/README.md b/README.md
index cba13733..0289fdf7 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,7 @@
 [![Crate](https://img.shields.io/crates/v/kibi.svg)](https://crates.io/crates/kibi)
 [![AUR](https://img.shields.io/aur/version/kibi.svg?logo=arch-linux)](https://aur.archlinux.org/packages/kibi/)
 [![Minimum rustc version](https://img.shields.io/badge/rustc-1.41+-blue.svg?logo=rust)](https://www.rust-lang.org/)
+[![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows%2010-blue)](#)
 [![License](https://img.shields.io/crates/l/kibi?color=blue)](#license)
 
 [![asciicast](https://gist.githubusercontent.com/ilai-deutel/39670157dd008d9932b2f2fd3c885cca/raw/bfdbfc96181c4f6e3ce2663c25c6e97bf57c8684/kibi.gif)](https://asciinema.org/a/KY7tKPlxHXqRdJiv5KaTJbPj5)
@@ -11,6 +12,8 @@
 A configurable text editor with UTF-8 support, incremental search, syntax highlighting, line numbers and more, written
 in less than 1024 lines<sup>[1](#counted-with)</sup> of Rust with minimal dependencies.
 
+Kibi is compatible with Linux, macOS, and Windows 10 (beta).
+
 This project is inspired by [`kilo`](https://github.com/antirez/kilo), a text editor written in C.
 See [comparison](#comparison-with-kilo) below for a list of additional features.
 
@@ -84,11 +87,15 @@ $ kibi <file path>
 
 #### Global configuration
 
-Kibi can be configured using:
-* A system-wide configuration file, located at `/etc/kibi/config.ini`
-* A user-level configuration file, located at:
-  * `$XDG_CONFIG_HOME/kibi/config.ini` if environment variable `$XDG_CONFIG_HOME` is defined
-  * `~/.config/kibi/config.ini` otherwise
+Kibi can be configured using configuration files. The location of these files is described below.
+
+* Linux / macOS:
+    * `/etc/kibi/config.ini` (system-wide configuration file)
+    * A user-level configuration file can be located located at:
+      * `$XDG_CONFIG_HOME/kibi/config.ini` if environment variable `$XDG_CONFIG_HOME` is defined
+      * `~/.config/kibi/config.ini` otherwise
+* Windows:
+    * `%APPDATA%\kibi\config.ini`
 
 Example configuration file:
 ```ini
@@ -106,10 +113,14 @@ show_line_numbers=true
 #### Syntax Highlighting
 
 Syntax highlighting can be configured using INI files located at:
-* `/etc/kibi/syntax.d/<file_name>.ini` for system-wide availability
-* For user-level configuration files:
-  * `$XDG_CONFIG_HOME/kibi/syntax.d/<file_name>.ini` if environment variable `$XDG_CONFIG_HOME` is defined
-  * `~/.config/kibi/syntax.d/<file_name>.ini` otherwise
+
+* Linux / macOS:
+    * `/etc/kibi/syntax.d/<file_name>.ini` for system-wide availability
+    * For user-level configuration files:
+      * `$XDG_CONFIG_HOME/kibi/syntax.d/<file_name>.ini` if environment variable `$XDG_CONFIG_HOME` is defined
+      * `~/.config/kibi/syntax.d/<file_name>.ini` otherwise
+* Windows:
+    * `%APPDATA%\kibi\syntax.d\<file_name>.ini`
 
 Syntax highlighting configuration follows this format:
 
@@ -137,12 +148,13 @@ This project is inspired by [`kilo`](https://github.com/antirez/kilo), a text ed
 
 `kibi` provides additional features:
 - Support for UTF-8 characters
+- Compatible with Windows
 - Command to jump to a given row/column
-- Handle window resize
+- Handle window resize (UNIX only)
 - Parsing configuration files: global editor configuration, language-specific syntax highlighting configuration
 - Display line numbers on the left of the screen; display file size in the status bar
 - Syntax highlighting: multi-line strings
-- "Save as" prompt when no file name has been provided
+- *Save as* prompt when no file name has been provided
 - Command to duplicate the current row
 - Memory safety, thanks to Rust!
 - Many bug fixes
@@ -152,11 +164,15 @@ This project is inspired by [`kilo`](https://github.com/antirez/kilo), a text ed
 This project must remain tiny, so using advanced dependencies such as [`ncurses`](https://crates.io/crates/ncurses),
 [`toml`](https://crates.io/crates/toml) or [`ansi-escapes`](https://crates.io/crates/ansi-escapes) would be cheating.
 
-These dependencies provide safe wrappers around `libc` calls, to avoid using `unsafe` code as much as possible:
+The following dependencies provide wrappers around system calls. Safe wrappers are preferred to avoid using `unsafe` code as much as possible:
 
-* `libc`
-* `nix`
-* `signal-hook`
+* On UNIX systems (Linux, macOS):
+    * `libc`
+    * `nix`
+    * `signal-hook`
+* On Windows:
+    * `winapi`
+    * `winapi-util`
 
 In addition, `unicode-width` is used to determine the displayed width of Unicode characters. Unfortunately, there is no
 way around it: the [unicode character width table](https://github.com/unicode-rs/unicode-width/blob/3033826f8bf05e82724140a981d5941e48fce393/src/tables.rs#L52)
diff --git a/rustfmt.toml b/rustfmt.toml
index 9e2426d3..9dfc4b2d 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -7,3 +7,4 @@ use_field_init_shorthand = true
 use_small_heuristics = "Max"
 use_try_shorthand = true
 where_single_line = true
+match_arm_blocks = false
diff --git a/src/config.rs b/src/config.rs
index c99df349..5f2efd25 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -2,7 +2,6 @@
 //!
 //! Utilities to configure the text editor.
 
-use std::env;
 use std::fmt::Display;
 use std::fs::File;
 use std::io::{BufRead, BufReader};
@@ -10,7 +9,7 @@ use std::path::{Path, PathBuf};
 use std::str::FromStr;
 use std::time::Duration;
 
-use crate::Error;
+use crate::{sys::conf_dirs, Error};
 
 /// The global Kibi configuration.
 pub struct Config {
@@ -27,20 +26,20 @@ pub struct Config {
     pub(crate) conf_dirs: Vec<PathBuf>,
 }
 
+/// Return directories that may contain configuration files for Kibi
+fn kibi_conf_dirs() -> Vec<PathBuf> {
+    conf_dirs().iter().filter_map(Option::as_ref).map(|p| PathBuf::from(p).join("kibi")).collect()
+}
+
 impl Default for Config {
     /// Default configuration.
     fn default() -> Self {
-        let conf_dirs = [
-            Some(String::from("/etc/kibi")),
-            env::var("XDG_CONFIG_HOME").map(|d| d + "/kibi").ok(),
-            env::var("HOME").map(|d| d + "/.config/kibi").ok(),
-        ];
         Self {
             tab_stop: 4,
             quit_times: 2,
             message_duration: Duration::from_secs(3),
             show_line_num: true,
-            conf_dirs: conf_dirs.iter().filter_map(|d| d.as_ref().map(PathBuf::from)).collect(),
+            conf_dirs: kibi_conf_dirs(),
         }
     }
 }
@@ -48,9 +47,12 @@ impl Default for Config {
 impl Config {
     /// Load the configuration, potentially overridden using `config.ini` files that can be located
     /// in the following directories:
-    ///   - `/etc/kibi` (system-wide configuration).
-    ///   - `$XDG_CONFIG_HOME/kibi` if environment variable `$XDG_CONFIG_HOME` is defined,
-    ///     `$HOME/.config/kibi` otherwise (user-level configuration).
+    ///   - On Linux, macOS, and other *nix systems:
+    ///     - `/etc/kibi` (system-wide configuration).
+    ///     - `$XDG_CONFIG_HOME/kibi` if environment variable `$XDG_CONFIG_HOME` is defined,
+    ///       `$HOME/.config/kibi` otherwise (user-level configuration).
+    ///   - On Windows:
+    ///     - `%APPDATA%/kibi`
     ///
     /// # Errors
     ///
@@ -58,10 +60,9 @@ impl Config {
     pub fn load() -> Result<Self, Error> {
         let mut conf = Self::default();
 
-        let conf_paths: Vec<PathBuf> =
-            conf.conf_dirs.iter().map(|p| p.join("config.ini")).filter(|p| p.exists()).collect();
+        let conf_paths: Vec<_> = conf.conf_dirs.iter().map(|p| p.join("config.ini")).collect();
 
-        for path in conf_paths {
+        for path in conf_paths.into_iter().filter(|p| p.exists()) {
             process_ini_file(&path, &mut |key, value| {
                 match key {
                     "tab_stop" => match parse_value(value)? {
@@ -69,9 +70,8 @@ impl Config {
                         tab_stop => conf.tab_stop = tab_stop,
                     },
                     "quit_times" => conf.quit_times = parse_value(value)?,
-                    "message_duration" => {
-                        conf.message_duration = Duration::from_secs_f32(parse_value(value)?)
-                    }
+                    "message_duration" =>
+                        conf.message_duration = Duration::from_secs_f32(parse_value(value)?),
                     "show_line_numbers" => conf.show_line_num = parse_value(value)?,
                     _ => return Err(format!("Invalid key: {}", key)),
                 };
diff --git a/src/editor.rs b/src/editor.rs
index 5f5a5cbe..53140819 100644
--- a/src/editor.rs
+++ b/src/editor.rs
@@ -1,13 +1,10 @@
 use std::io::{self, BufRead, BufReader, ErrorKind::NotFound, Read, Seek, Write};
 use std::iter::{self, repeat, successors};
-use std::sync::mpsc::{self, Receiver, TryRecvError};
+use std::sync::mpsc::{Receiver, TryRecvError};
 use std::{fmt::Display, fs::File, path::Path, time::Instant};
 
-use nix::sys::termios::Termios;
-use signal_hook::{iterator::Signals, SIGWINCH};
-
 use crate::row::{HLState, Row};
-use crate::{ansi_escape::*, syntax::SyntaxConf, terminal, Config, Error};
+use crate::{ansi_escape::*, syntax::SyntaxConf, sys, Config, Error};
 
 const fn ctrl_key(key: u8) -> u8 { key & 0x1f }
 const EXIT: u8 = ctrl_key(b'Q');
@@ -106,8 +103,8 @@ pub struct Editor {
     /// A channel receiver for the "window size changed" message. A message is received shortly
     /// after a SIGWINCH signal is received.
     ws_changed_receiver: Option<Receiver<()>>,
-    /// The original termios configuration. It will be restored when the `Editor` is dropped.
-    orig_termios: Option<Termios>,
+    /// The original terminal mode. It will be restored when the `Editor` instance is dropped.
+    orig_term_mode: Option<sys::TermMode>,
 }
 
 /// Describes a status message, shown at the bottom at the screen.
@@ -140,21 +137,13 @@ impl Editor {
     /// Will return `Err` if an error occurs when enabling termios raw mode, creating the signal hook
     /// or when obtaining the terminal window size.
     pub fn new(config: Config) -> Result<Self, Error> {
-        // Create a channel for receiving window size update requests
-        let (ws_changed_tx, ws_changed_rx) = mpsc::sync_channel(1);
-        // Spawn a new thread that will push to the aforementioned channel every time the SIGWINCH
-        // signal is received
-        let signals = Signals::new(&[SIGWINCH])?;
-        std::thread::spawn(move || signals.forever().for_each(|_| ws_changed_tx.send(()).unwrap()));
-
         let mut editor = Self::default();
         editor.quit_times = config.quit_times;
         editor.config = config;
-        editor.ws_changed_receiver = Some(ws_changed_rx);
-
-        // Enable termios raw mode and store the original (non-raw) termios.
-        editor.orig_termios = Some(terminal::enable_raw_mode()?);
+        editor.ws_changed_receiver = sys::get_window_size_update_receiver()?;
 
+        // Enable raw mode and store the original (non-raw) terminal mode.
+        editor.orig_term_mode = Some(sys::enable_raw_mode()?);
         editor.update_window_size()?;
 
         set_status!(editor, "{}", HELP_MESSAGE);
@@ -172,9 +161,8 @@ impl Editor {
     /// Move the cursor following an arrow key (← → ↑ ↓).
     fn move_cursor(&mut self, key: &AKey) {
         match (key, self.current_row()) {
-            (AKey::Left, Some(row)) if self.cursor.x > 0 => {
-                self.cursor.x -= row.get_char_size(row.cx2rx[self.cursor.x] - 1)
-            }
+            (AKey::Left, Some(row)) if self.cursor.x > 0 =>
+                self.cursor.x -= row.get_char_size(row.cx2rx[self.cursor.x] - 1),
             (AKey::Left, _) if self.cursor.y > 0 => {
                 // ← at the beginning of the line: move to the end of the previous line. The x
                 // position will be adjusted after this `match` to accommodate the current row
@@ -182,9 +170,8 @@ impl Editor {
                 self.cursor.y -= 1;
                 self.cursor.x = usize::max_value();
             }
-            (AKey::Right, Some(row)) if self.cursor.x < row.chars.len() => {
-                self.cursor.x += row.get_char_size(row.cx2rx[self.cursor.x])
-            }
+            (AKey::Right, Some(row)) if self.cursor.x < row.chars.len() =>
+                self.cursor.x += row.get_char_size(row.cx2rx[self.cursor.x]),
             (AKey::Right, Some(_)) => {
                 // Move to the next line
                 self.cursor.y += 1;
@@ -275,7 +262,7 @@ impl Editor {
 
     /// Update the `screen_rows`, `window_width`, `screen_cols` and `ln_padding` attributes.
     fn update_window_size(&mut self) -> Result<(), Error> {
-        let (window_height, window_width) = terminal::get_window_size()?;
+        let (window_height, window_width) = sys::get_window_size()?;
         self.screen_rows = window_height.saturating_sub(2); // Make room for the status bar and status message
         self.window_width = window_width;
         self.update_screen_cols();
@@ -575,9 +562,9 @@ impl Editor {
             // If in prompt mode, position the cursor on the prompt line at the end of the line.
             (self.status_msg.as_ref().map_or(0, |sm| sm.msg.len() + 1), self.screen_rows + 2)
         };
-        // Move the cursor
-        buffer.push_str(&format!("\x1b[{};{}H{}", cursor_y, cursor_x, SHOW_CURSOR));
-        terminal::print_and_flush(&buffer)
+        // Finally, print `buffer` and move the cursor
+        print!("{}\x1b[{};{}H{}", buffer, cursor_y, cursor_x, SHOW_CURSOR);
+        io::stdout().flush().map_err(Error::from)
     }
 
     /// Process a key that has been pressed, when not in prompt mode. Returns whether the program
@@ -623,9 +610,8 @@ impl Editor {
                 }
                 None => prompt_mode = Some(PromptMode::Save(String::new())),
             },
-            Key::Char(FIND) => {
-                prompt_mode = Some(PromptMode::Find(String::new(), self.cursor.clone(), None))
-            }
+            Key::Char(FIND) =>
+                prompt_mode = Some(PromptMode::Find(String::new(), self.cursor.clone(), None)),
             Key::Char(GOTO) => prompt_mode = Some(PromptMode::GoTo(String::new())),
             Key::Char(DUPLICATE) => self.duplicate_current_row(),
             Key::Char(c) => self.insert_byte(*c),
@@ -690,10 +676,10 @@ impl Editor {
 }
 
 impl Drop for Editor {
-    /// When the editor is dropped, restore the original termios.
+    /// When the editor is dropped, restore the original terminal mode.
     fn drop(&mut self) {
-        if let Some(orig_termios) = &self.orig_termios {
-            terminal::set_termios(orig_termios).expect("Could not restore original termios.")
+        if let Some(orig_term_mode) = self.orig_term_mode.take() {
+            sys::set_term_mode(&orig_term_mode).expect("Could not restore original terminal mode.")
         }
     }
 }
@@ -736,9 +722,8 @@ impl PromptMode {
                 match process_prompt_keypress(b, key) {
                     PromptState::Active(query) => {
                         let (last_match, forward) = match key {
-                            Key::Arrow(AKey::Right) | Key::Arrow(AKey::Down) | Key::Char(FIND) => {
-                                (last_match, true)
-                            }
+                            Key::Arrow(AKey::Right) | Key::Arrow(AKey::Down) | Key::Char(FIND) =>
+                                (last_match, true),
                             Key::Arrow(AKey::Left) | Key::Arrow(AKey::Up) => (last_match, false),
                             _ => (None, true),
                         };
diff --git a/src/error.rs b/src/error.rs
index 9e6cb130..4c1fbe41 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -5,6 +5,7 @@ use std::fmt::{self, Display, Formatter};
 /// These errors can used in the program.
 pub enum Error {
     IO(std::io::Error),
+    #[cfg(unix)]
     Nix(nix::Error),
     MPSCTryRecv(std::sync::mpsc::TryRecvError),
     CursorPosition,
@@ -17,6 +18,7 @@ impl Display for Error {
     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
         match self {
             Self::IO(err) => write!(f, "IO error: {}", err),
+            #[cfg(unix)]
             Self::Nix(err) => write!(f, "System call error: {}", err),
             Self::MPSCTryRecv(err) => write!(f, "MSPC try_recv error: {}", err),
             Self::CursorPosition => write!(f, "Could not obtain cursor position"),
@@ -30,6 +32,8 @@ impl From<std::io::Error> for Error {
     /// Convert an IO Error into a Kibi Error.
     fn from(err: std::io::Error) -> Self { Self::IO(err) }
 }
+
+#[cfg(unix)]
 impl From<nix::Error> for Error {
     /// Convert a nix IO Error into a Kibi Error.
     fn from(err: nix::Error) -> Self { Self::Nix(err) }
diff --git a/src/lib.rs b/src/lib.rs
index 9e575879..bd329e37 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -11,3 +11,9 @@ mod error;
 mod row;
 mod syntax;
 mod terminal;
+
+#[cfg(windows)] mod windows;
+#[cfg(windows)] use windows as sys;
+
+#[cfg(unix)] mod unix;
+#[cfg(unix)] use unix as sys;
diff --git a/src/syntax.rs b/src/syntax.rs
index dad21a20..74ae0341 100644
--- a/src/syntax.rs
+++ b/src/syntax.rs
@@ -53,14 +53,13 @@ impl SyntaxConf {
     pub(crate) fn get(ext: &str, conf_dirs: &[PathBuf]) -> Result<Option<Self>, Error> {
         for conf_dir in conf_dirs.iter().rev() {
             match conf_dir.join("syntax.d").read_dir() {
-                Ok(dir_entries) => {
+                Ok(dir_entries) =>
                     for dir_entry in dir_entries {
                         let (sc, extensions) = Self::from_file(&dir_entry?.path())?;
                         if extensions.into_iter().any(|e| e == ext) {
                             return Ok(Some(sc));
                         };
-                    }
-                }
+                    },
                 Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
                 Err(e) => return Err(e.into()),
             }
diff --git a/src/terminal.rs b/src/terminal.rs
index a3728ade..c8b6fd32 100644
--- a/src/terminal.rs
+++ b/src/terminal.rs
@@ -1,69 +1,15 @@
 use std::io::{self, BufRead, Read, Write};
 
-use libc::{STDIN_FILENO, STDOUT_FILENO, TIOCGWINSZ, VMIN, VTIME};
-use nix::{pty::Winsize, sys::termios};
-
 use crate::{ansi_escape::DEVICE_STATUS_REPORT, ansi_escape::REPOSITION_CURSOR_END, Error};
 
-pub(crate) fn set_termios(term: &termios::Termios) -> Result<(), nix::Error> {
-    termios::tcsetattr(STDIN_FILENO, termios::SetArg::TCSAFLUSH, term)
-}
-
-/// Setup the termios to enable raw mode, and return the original termios.
-///
-/// termios manual is available at: <http://man7.org/linux/man-pages/man3/termios.3.html>
-pub(crate) fn enable_raw_mode() -> Result<termios::Termios, Error> {
-    let orig_termios = termios::tcgetattr(STDIN_FILENO)?;
-    let mut term = orig_termios.clone();
-    termios::cfmakeraw(&mut term);
-    // Set the minimum number of characters for non-canonical reads
-    term.control_chars[VMIN] = 0;
-    // Set the timeout in deciseconds for non-canonical reads
-    term.control_chars[VTIME] = 1;
-    set_termios(&term)?;
-    Ok(orig_termios)
-}
-
-/// Return the current window size as (rows, columns).
+/// Obtain the window size using the cursor position.
 ///
-/// Two methods are used to get the window size:
-/// 1. Use the TIOCGWINSZ to get window size. If it succeeds, a Winsize struct will be populated
-///    This ioctl is described here: <http://man7.org/linux/man-pages/man4/tty_ioctl.4.html>
-/// 2. If the first method fails, we reposition the cursor at the end of the terminal and get the
-///    cursor position.
-pub(crate) fn get_window_size() -> Result<(usize, usize), Error> {
-    nix::ioctl_read_bad!(get_ws, TIOCGWINSZ, Winsize);
-    let mut maybe_ws = std::mem::MaybeUninit::<Winsize>::uninit();
-
-    // Alternate method to get the window size, if TIOCGWINSZ ioctl calls fails: reposition the
-    // cursor at the end and obtain the cursor position.
-    let get_window_size_v2 =
-        || print_and_flush(REPOSITION_CURSOR_END).and_then(|_| get_cursor_position());
-
-    unsafe { get_ws(STDOUT_FILENO, maybe_ws.as_mut_ptr()).ok().map(|_| maybe_ws.assume_init()) }
-        .filter(|ws| ws.ws_col != 0 && ws.ws_row != 0)
-        // If the IOCTL method fails, use the alternate method
-        .map_or_else(get_window_size_v2, |ws| Ok((ws.ws_row as usize, ws.ws_col as usize)))
-}
-
-/// Read value until a certain stop byte is reached, and parse the result (pre-stop byte).
-fn read_value_until<T: std::str::FromStr>(stop_byte: u8) -> Result<T, Error> {
-    let mut buffer = Vec::new();
-    io::stdin().lock().read_until(stop_byte, &mut buffer)?;
-    // Check that we have reached `stop_byte`, not EOF.
-    if buffer.pop().filter(|u| *u == stop_byte).is_none() {
-        return Err(Error::CursorPosition);
-    }
-    std::str::from_utf8(&buffer)
-        .or(Err(Error::CursorPosition))?
-        .parse()
-        .or(Err(Error::CursorPosition))
-}
-
-/// Return the position (row, column) of the cursor.
-fn get_cursor_position() -> Result<(usize, usize), Error> {
-    // DEVICE_STATUS_REPORT reports the cursor position as ESC[{row};{column}R
-    print_and_flush(DEVICE_STATUS_REPORT)?;
+/// This function moves the cursor to the bottom-right using ANSI escape sequence
+/// `\x1b[999C\x1b[999B`, then requests the cursor position using ANSI escape sequence `\x1b[6n`.
+/// After this sequence is sent, the next characters on stdin should be `\x1b[{row};{column}R`.
+pub(crate) fn get_window_size_using_cursor() -> Result<(usize, usize), Error> {
+    print!("{}{}", REPOSITION_CURSOR_END, DEVICE_STATUS_REPORT);
+    io::stdout().flush()?;
     let mut prefix_buffer = [0_u8; 2];
     io::stdin().read_exact(&mut prefix_buffer)?;
     if prefix_buffer != [b'\x1b', b'['] {
@@ -72,8 +18,13 @@ fn get_cursor_position() -> Result<(usize, usize), Error> {
     Ok((read_value_until(b';')?, read_value_until(b'R')?))
 }
 
-/// Print a string to stdout and flush.
-pub(crate) fn print_and_flush(s: &str) -> Result<(), Error> {
-    io::stdout().write_all(s.as_bytes())?;
-    io::stdout().flush().map_err(Error::from)
+/// Read value until a certain stop byte is reached, and parse the result (pre-stop byte).
+fn read_value_until<T: std::str::FromStr>(stop_byte: u8) -> Result<T, Error> {
+    let mut buf = Vec::new();
+    io::stdin().lock().read_until(stop_byte, &mut buf)?;
+    // Check that we have reached `stop_byte`, not EOF.
+    if buf.pop().filter(|u| *u == stop_byte).is_none() {
+        return Err(Error::CursorPosition);
+    }
+    std::str::from_utf8(&buf).or(Err(Error::CursorPosition))?.parse().or(Err(Error::CursorPosition))
 }
diff --git a/src/unix.rs b/src/unix.rs
new file mode 100644
index 00000000..9a942589
--- /dev/null
+++ b/src/unix.rs
@@ -0,0 +1,69 @@
+//! # sys (UNIX)
+//!
+//! UNIX-specific structs and functions. Will be imported as `sys` on UNIX systems.
+
+use std::env::var;
+use std::sync::mpsc::{self, Receiver};
+
+use libc::{STDIN_FILENO, STDOUT_FILENO, TIOCGWINSZ, VMIN, VTIME};
+use nix::{pty::Winsize, sys::termios};
+use signal_hook::{iterator::Signals, SIGWINCH};
+
+// On UNIX systems, Termios represents the terminal mode.
+pub(crate) use nix::sys::termios::Termios as TermMode;
+
+use crate::{terminal::get_window_size_using_cursor, Error};
+
+/// Return configuration directories for UNIX systems
+pub(crate) fn conf_dirs() -> [Option<String>; 3] {
+    [Some("/etc".into()), var("XDG_CONFIG_HOME").ok(), var("HOME").map(|d| d + "/.config").ok()]
+}
+
+/// Return the current window size as (rows, columns).
+///
+/// Two methods are used to get the window size:
+/// 1. Use the TIOCGWINSZ to get window size. If it succeeds, a Winsize struct will be populated
+///    This ioctl is described here: <http://man7.org/linux/man-pages/man4/tty_ioctl.4.html>
+/// 2. If the first method fails, we reposition the cursor at the end of the terminal and get the
+///    cursor position.
+pub(crate) fn get_window_size() -> Result<(usize, usize), Error> {
+    nix::ioctl_read_bad!(get_ws, TIOCGWINSZ, Winsize);
+
+    let mut maybe_ws = std::mem::MaybeUninit::<Winsize>::uninit();
+
+    unsafe { get_ws(STDOUT_FILENO, maybe_ws.as_mut_ptr()).ok().map(|_| maybe_ws.assume_init()) }
+        .filter(|ws| ws.ws_col != 0 && ws.ws_row != 0)
+        // If the IOCTL method fails, use the alternate method
+        .map_or_else(get_window_size_using_cursor, |ws| Ok((ws.ws_row as usize, ws.ws_col as usize)))
+}
+
+/// Return a MPSC receiver that receives a message whenever the window size is updated.
+pub(crate) fn get_window_size_update_receiver() -> Result<Option<Receiver<()>>, Error> {
+    // Create a channel for receiving window size update requests
+    let (ws_changed_tx, ws_changed_rx) = mpsc::sync_channel(1);
+    // Spawn a new thread that will push to the aforementioned channel every time the SIGWINCH
+    // signal is received
+    let signals = Signals::new(&[SIGWINCH])?;
+    std::thread::spawn(move || signals.forever().for_each(|_| ws_changed_tx.send(()).unwrap()));
+    Ok(Some(ws_changed_rx))
+}
+
+/// Set the terminal mode.
+pub(crate) fn set_term_mode(term: &TermMode) -> Result<(), nix::Error> {
+    termios::tcsetattr(STDIN_FILENO, termios::SetArg::TCSAFLUSH, term)
+}
+
+/// Setup the termios to enable raw mode, and return the original termios.
+///
+/// termios manual is available at: <http://man7.org/linux/man-pages/man3/termios.3.html>
+pub(crate) fn enable_raw_mode() -> Result<TermMode, Error> {
+    let orig_termios = termios::tcgetattr(STDIN_FILENO)?;
+    let mut term = orig_termios.clone();
+    termios::cfmakeraw(&mut term);
+    // Set the minimum number of characters for non-canonical reads
+    term.control_chars[VMIN] = 0;
+    // Set the timeout in deciseconds for non-canonical reads
+    term.control_chars[VTIME] = 1;
+    set_term_mode(&term)?;
+    Ok(orig_termios)
+}
diff --git a/src/windows.rs b/src/windows.rs
new file mode 100644
index 00000000..6eb73926
--- /dev/null
+++ b/src/windows.rs
@@ -0,0 +1,34 @@
+//! # sys (Windows)
+//!
+//! Windows-specific structs and functions. Will be imported as `sys` on Windows systems.
+
+use std::sync::mpsc::Receiver;
+
+use winapi::um::wincon::*;
+
+pub(crate) use crate::{terminal::get_window_size_using_cursor as get_window_size, Error};
+
+// On Windows systems, the terminal mode is represented as an unsigned int.
+pub(crate) type TermMode = u32;
+
+/// Return configuration directories for Windows systems
+pub(crate) fn conf_dirs() -> [Option<String>; 1] { [std::env::var("APPDATA").ok()] }
+
+pub(crate) fn get_window_size_update_receiver() -> Result<Option<Receiver<()>>, Error> { Ok(None) }
+
+/// Set the terminal mode.
+pub(crate) fn set_term_mode(stdin_term_mode: &TermMode) -> Result<(), std::io::Error> {
+    winapi_util::console::set_mode(winapi_util::HandleRef::stdin(), *stdin_term_mode)
+}
+
+/// Enable raw mode, and return the original terminal mode.
+///
+/// Documentation for console modes is available at:
+/// <https://docs.microsoft.com/en-us/windows/console/setconsolemodel>
+pub(crate) fn enable_raw_mode() -> Result<TermMode, Error> {
+    let orig_mode_in = winapi_util::console::mode(winapi_util::HandleRef::stdin())?;
+    let mode_in = (orig_mode_in | ENABLE_VIRTUAL_TERMINAL_INPUT)
+        & !(ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT);
+    set_term_mode(&mode_in)?;
+    Ok(orig_mode_in)
+}


Test patch: