diff --git a/Cargo.lock b/Cargo.lock
index 5688fd15..5a5fcf4b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -378,6 +378,8 @@ dependencies = [
  "pulldown-cmark",
  "serde",
  "serde_json",
+ "signal-hook",
+ "signal-hook-tokio",
  "tokio",
  "toml",
 ]
@@ -865,6 +867,18 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "signal-hook-tokio"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6c5d32165ff8b94e68e7b3bdecb1b082e958c22434b363482cfb89dcd6f3ff8"
+dependencies = [
+ "futures-core",
+ "libc",
+ "signal-hook",
+ "tokio",
+]
+
 [[package]]
 name = "similar"
 version = "1.3.0"
diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml
index cf1f30a7..ef9feb94 100644
--- a/helix-lsp/Cargo.toml
+++ b/helix-lsp/Cargo.toml
@@ -23,5 +23,5 @@ lsp-types = { version = "0.89", features = ["proposed"] }
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 thiserror = "1.0"
-tokio = { version = "1.9", features = ["full"] }
+tokio = { version = "1.9", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
 tokio-stream = "0.1.7"
diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml
index 1fc14ad2..0e2baae3 100644
--- a/helix-term/Cargo.toml
+++ b/helix-term/Cargo.toml
@@ -28,10 +28,11 @@ helix-lsp = { version = "0.3", path = "../helix-lsp" }
 anyhow = "1"
 once_cell = "1.8"
 
-tokio = { version = "1", features = ["full"] }
+tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
 num_cpus = "1"
 tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
 crossterm = { version = "0.20", features = ["event-stream"] }
+signal-hook = "0.3"
 
 futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
 
@@ -53,3 +54,6 @@ toml = "0.5"
 
 serde_json = "1.0"
 serde = { version = "1.0", features = ["derive"] }
+
+[target.'cfg(not(windows))'.dependencies]  # https://github.com/vorner/signal-hook/issues/100
+signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 5f350671..9e659fb0 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -18,6 +18,10 @@ use crossterm::{
     event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
     execute, terminal,
 };
+#[cfg(not(windows))]
+use signal_hook::{consts::signal, low_level};
+#[cfg(not(windows))]
+use signal_hook_tokio::Signals;
 
 pub struct Application {
     compositor: Compositor,
@@ -36,6 +40,8 @@ pub struct Application {
     #[allow(dead_code)]
     syn_loader: Arc<syntax::Loader>,
 
+    #[cfg(not(windows))]
+    signals: Signals,
     jobs: Jobs,
     lsp_progress: LspProgressMap,
 }
@@ -102,6 +108,9 @@ impl Application {
 
         editor.set_theme(theme);
 
+        #[cfg(not(windows))]
+        let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT])?;
+
         let app = Self {
             compositor,
             editor,
@@ -111,6 +120,8 @@ impl Application {
             theme_loader,
             syn_loader,
 
+            #[cfg(not(windows))]
+            signals,
             jobs: Jobs::new(),
             lsp_progress: LspProgressMap::new(),
         };
@@ -147,6 +158,51 @@ impl Application {
 
             use futures_util::StreamExt;
 
+            #[cfg(not(windows))]
+            tokio::select! {
+                biased;
+
+                event = reader.next() => {
+                    self.handle_terminal_events(event)
+                }
+                Some(signal) = self.signals.next() => {
+                    use helix_view::graphics::Rect;
+                    match signal {
+                        signal::SIGTSTP => {
+                            self.compositor.save_cursor();
+                            self.restore_term().unwrap();
+                            low_level::emulate_default_handler(signal::SIGTSTP).unwrap();
+                        }
+                        signal::SIGCONT => {
+                            self.claim_term().await.unwrap();
+                            // redraw the terminal
+                            let Rect { width, height, .. } = self.compositor.size();
+                            self.compositor.resize(width, height);
+                            self.compositor.load_cursor();
+                            self.render();
+                        }
+                        _ => unreachable!(),
+                    }
+                }
+                Some((id, call)) = self.editor.language_servers.incoming.next() => {
+                    self.handle_language_server_message(call, id).await;
+                    // limit render calls for fast language server messages
+                    let last = self.editor.language_servers.incoming.is_empty();
+                    if last || last_render.elapsed() > deadline {
+                        self.render();
+                        last_render = Instant::now();
+                    }
+                }
+                Some(callback) = self.jobs.futures.next() => {
+                    self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
+                    self.render();
+                }
+                Some(callback) = self.jobs.wait_futures.next() => {
+                    self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
+                    self.render();
+                }
+            }
+            #[cfg(windows)]
             tokio::select! {
                 biased;
 
@@ -443,15 +499,29 @@ impl Application {
         }
     }
 
-    pub async fn run(&mut self) -> Result<(), Error> {
+    async fn claim_term(&mut self) -> Result<(), Error> {
         terminal::enable_raw_mode()?;
-
         let mut stdout = stdout();
-
         execute!(stdout, terminal::EnterAlternateScreen)?;
+        self.editor.close_language_servers(None).await?;
         if self.config.terminal.mouse {
             execute!(stdout, EnableMouseCapture)?;
         }
+        Ok(())
+    }
+
+    fn restore_term(&mut self) -> Result<(), Error> {
+        let mut stdout = stdout();
+        // reset cursor shape
+        write!(stdout, "\x1B[2 q")?;
+        execute!(stdout, DisableMouseCapture)?;
+        execute!(stdout, terminal::LeaveAlternateScreen)?;
+        terminal::disable_raw_mode()?;
+        Ok(())
+    }
+
+    pub async fn run(&mut self) -> Result<(), Error> {
+        self.claim_term().await?;
 
         // Exit the alternate screen and disable raw mode before panicking
         let hook = std::panic::take_hook();
@@ -469,13 +539,7 @@ impl Application {
 
         self.editor.close_language_servers(None).await?;
 
-        // reset cursor shape
-        write!(stdout, "\x1B[2 q")?;
-
-        execute!(stdout, DisableMouseCapture)?;
-        execute!(stdout, terminal::LeaveAlternateScreen)?;
-
-        terminal::disable_raw_mode()?;
+        self.restore_term()?;
 
         Ok(())
     }
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index cc2c2cca..5cbab34f 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -291,7 +291,8 @@ impl Command {
         surround_replace, "Surround replace",
         surround_delete, "Surround delete",
         select_textobject_around, "Select around object",
-        select_textobject_inner, "Select inside object"
+        select_textobject_inner, "Select inside object",
+        suspend, "Suspend"
     );
 }
 
@@ -3894,3 +3895,8 @@ fn surround_delete(cx: &mut Context) {
         }
     })
 }
+
+fn suspend(_cx: &mut Context) {
+    #[cfg(not(windows))]
+    signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap();
+}
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index c2cfa3a7..628c4e13 100644
--- a/helix-term/src/compositor.rs
+++ b/helix-term/src/compositor.rs
@@ -68,7 +68,7 @@ pub trait Component: Any + AnyComponent {
 
 use anyhow::Error;
 use std::io::stdout;
-use tui::backend::CrosstermBackend;
+use tui::backend::{Backend, CrosstermBackend};
 type Terminal = tui::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
 
 pub struct Compositor {
@@ -99,6 +99,21 @@ impl Compositor {
             .expect("Unable to resize terminal")
     }
 
+    pub fn save_cursor(&mut self) {
+        if self.terminal.cursor_kind() == CursorKind::Hidden {
+            self.terminal
+                .backend_mut()
+                .show_cursor(CursorKind::Block)
+                .ok();
+        }
+    }
+
+    pub fn load_cursor(&mut self) {
+        if self.terminal.cursor_kind() == CursorKind::Hidden {
+            self.terminal.backend_mut().hide_cursor().ok();
+        }
+    }
+
     pub fn push(&mut self, mut layer: Box<dyn Component>) {
         let size = self.size();
         // trigger required_size on init
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 053b92e6..5fe730a1 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -502,6 +502,7 @@ impl Default for Keymaps {
             },
 
             "\"" => select_register,
+            "C-z" => suspend,
         });
         // TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
         // we keep this separate select mode. More keys can fit into normal mode then, but it's weird
diff --git a/helix-tui/src/terminal.rs b/helix-tui/src/terminal.rs
index 4637eb71..22e9232f 100644
--- a/helix-tui/src/terminal.rs
+++ b/helix-tui/src/terminal.rs
@@ -45,8 +45,8 @@ where
     buffers: [Buffer; 2],
     /// Index of the current buffer in the previous array
     current: usize,
-    /// Whether the cursor is currently hidden
-    hidden_cursor: bool,
+    /// Kind of cursor (hidden or others)
+    cursor_kind: CursorKind,
     /// Viewport
     viewport: Viewport,
 }
@@ -57,7 +57,7 @@ where
 {
     fn drop(&mut self) {
         // Attempt to restore the cursor state
-        if self.hidden_cursor {
+        if self.cursor_kind == CursorKind::Hidden {
             if let Err(err) = self.show_cursor(CursorKind::Block) {
                 eprintln!("Failed to show the cursor: {}", err);
             }
@@ -93,7 +93,7 @@ where
                 Buffer::empty(options.viewport.area),
             ],
             current: 0,
-            hidden_cursor: false,
+            cursor_kind: CursorKind::Block,
             viewport: options.viewport,
         })
     }
@@ -185,15 +185,20 @@ where
         Ok(())
     }
 
+    #[inline]
+    pub fn cursor_kind(&self) -> CursorKind {
+        self.cursor_kind
+    }
+
     pub fn hide_cursor(&mut self) -> io::Result<()> {
         self.backend.hide_cursor()?;
-        self.hidden_cursor = true;
+        self.cursor_kind = CursorKind::Hidden;
         Ok(())
     }
 
     pub fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
         self.backend.show_cursor(kind)?;
-        self.hidden_cursor = false;
+        self.cursor_kind = kind;
         Ok(())
     }
 
diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml
index 89e183f0..3617506f 100644
--- a/helix-view/Cargo.toml
+++ b/helix-view/Cargo.toml
@@ -24,7 +24,7 @@ crossterm = { version = "0.20", optional = true }
 once_cell = "1.8"
 url = "2"
 
-tokio = { version = "1", features = ["full"] }
+tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
 futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
 
 slotmap = "1"
diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs
index e14ce2b9..5138e923 100644
--- a/helix-view/src/graphics.rs
+++ b/helix-view/src/graphics.rs
@@ -1,7 +1,7 @@
 use bitflags::bitflags;
 use std::cmp::{max, min};
 
-#[derive(Debug)]
+#[derive(Debug, Clone, Copy, PartialEq)]
 /// UNSTABLE
 pub enum CursorKind {
     /// █