diff --git a/espanso/src/cli/worker/engine/dispatch/executor/mod.rs b/espanso/src/cli/worker/engine/dispatch/executor/mod.rs
index 7fee77a..ff3dc19 100644
--- a/espanso/src/cli/worker/engine/dispatch/executor/mod.rs
+++ b/espanso/src/cli/worker/engine/dispatch/executor/mod.rs
@@ -23,6 +23,7 @@ pub mod event_injector;
 pub mod icon;
 pub mod key_injector;
 pub mod secure_input;
+pub mod text_ui;
 
 pub trait InjectParamsProvider {
   fn get(&self) -> InjectParams;
diff --git a/espanso/src/cli/worker/engine/dispatch/executor/text_ui.rs b/espanso/src/cli/worker/engine/dispatch/executor/text_ui.rs
new file mode 100644
index 0000000..5d73932
--- /dev/null
+++ b/espanso/src/cli/worker/engine/dispatch/executor/text_ui.rs
@@ -0,0 +1,39 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_engine::dispatch::TextUIHandler;
+
+use crate::gui::TextUI;
+
+pub struct TextUIHandlerAdapter<'a> {
+  text_ui: &'a dyn TextUI,
+}
+
+impl<'a> TextUIHandlerAdapter<'a> {
+  pub fn new(text_ui: &'a dyn TextUI) -> Self {
+    Self { text_ui }
+  }
+}
+
+impl<'a> TextUIHandler for TextUIHandlerAdapter<'a> {
+  fn show_text(&self, title: &str, text: &str) -> anyhow::Result<()> {
+    self.text_ui.show_text(title, text)?;
+    Ok(())
+  }
+}
diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs
index 07c653a..0cd74b2 100644
--- a/espanso/src/cli/worker/engine/mod.rs
+++ b/espanso/src/cli/worker/engine/mod.rs
@@ -37,6 +37,7 @@ use crate::{
         clipboard_injector::ClipboardInjectorAdapter, context_menu::ContextMenuHandlerAdapter,
         event_injector::EventInjectorAdapter, icon::IconHandlerAdapter,
         key_injector::KeyInjectorAdapter, secure_input::SecureInputManagerAdapter,
+        text_ui::TextUIHandlerAdapter,
       },
       process::middleware::{
         image_resolve::PathProviderAdapter,
@@ -106,6 +107,7 @@ pub fn initialize_and_spawn(
       let modulo_manager = crate::gui::modulo::manager::ModuloManager::new();
       let modulo_form_ui = crate::gui::modulo::form::ModuloFormUI::new(&modulo_manager);
       let modulo_search_ui = crate::gui::modulo::search::ModuloSearchUI::new(&modulo_manager);
+      let modulo_text_ui = crate::gui::modulo::textview::ModuloTextUI::new(&modulo_manager);
 
       let context: Box<dyn Context> = Box::new(super::context::DefaultContext::new(
         &config_manager,
@@ -241,6 +243,7 @@ pub fn initialize_and_spawn(
       let context_menu_adapter = ContextMenuHandlerAdapter::new(&*ui_remote);
       let icon_adapter = IconHandlerAdapter::new(&*ui_remote);
       let secure_input_adapter = SecureInputManagerAdapter::new();
+      let text_ui_adapter = TextUIHandlerAdapter::new(&modulo_text_ui);
       let dispatcher = espanso_engine::dispatch::default(
         &event_injector,
         &clipboard_injector,
@@ -251,6 +254,7 @@ pub fn initialize_and_spawn(
         &context_menu_adapter,
         &icon_adapter,
         &secure_input_adapter,
+        &text_ui_adapter,
       );
 
       // Disable previously granted linux capabilities if not needed anymore
diff --git a/espanso/src/gui/mod.rs b/espanso/src/gui/mod.rs
index bcd26e7..86ae5db 100644
--- a/espanso/src/gui/mod.rs
+++ b/espanso/src/gui/mod.rs
@@ -17,7 +17,7 @@
  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::collections::HashMap;
+use std::{collections::HashMap, path::Path};
 
 use anyhow::Result;
 
@@ -58,3 +58,8 @@ pub enum FormField {
     values: Vec<String>,
   },
 }
+
+pub trait TextUI {
+  fn show_text(&self, title: &str, text: &str) -> Result<()>;
+  fn show_file(&self, title: &str, path: &Path) -> Result<()>;
+}
diff --git a/espanso/src/gui/modulo/manager.rs b/espanso/src/gui/modulo/manager.rs
index 0cc3184..e371db9 100644
--- a/espanso/src/gui/modulo/manager.rs
+++ b/espanso/src/gui/modulo/manager.rs
@@ -39,6 +39,52 @@ impl ModuloManager {
     Self { is_support_enabled }
   }
 
+  pub fn invoke_no_output(&self, args: &[&str], body: &str) -> Result<()> {
+    if self.is_support_enabled {
+      let exec_path = std::env::current_exe().expect("unable to obtain current exec path");
+      let mut command = Command::new(exec_path);
+      let mut full_args = vec!["modulo"];
+      full_args.extend(args);
+      command
+        .args(full_args)
+        .stdin(std::process::Stdio::piped())
+        .stdout(std::process::Stdio::piped())
+        .stderr(std::process::Stdio::piped());
+
+      crate::util::set_command_flags(&mut command);
+
+      let child = command.spawn();
+
+      match child {
+        Ok(mut child) => {
+          if let Some(stdin) = child.stdin.as_mut() {
+            match stdin.write_all(body.as_bytes()) {
+              Ok(_) => {
+                // Get the output
+                match child.wait_with_output() {
+                  Ok(child_output) => {
+                    if child_output.status.success() {
+                      Ok(())
+                    } else {
+                      Err(ModuloError::NonZeroExit.into())
+                    }
+                  }
+                  Err(error) => Err(ModuloError::Error(error).into()),
+                }
+              }
+              Err(error) => Err(ModuloError::Error(error).into()),
+            }
+          } else {
+            Err(ModuloError::StdinError.into())
+          }
+        }
+        Err(error) => Err(ModuloError::Error(error).into()),
+      }
+    } else {
+      Err(ModuloError::MissingModulo.into())
+    }
+  }
+
   pub fn invoke(&self, args: &[&str], body: &str) -> Result<String> {
     if self.is_support_enabled {
       let exec_path = std::env::current_exe().expect("unable to obtain current exec path");
@@ -101,6 +147,9 @@ pub enum ModuloError {
   )]
   MissingModulo,
 
+  #[error("modulo returned a non-zero exit code")]
+  NonZeroExit,
+
   #[error("modulo returned an empty output")]
   EmptyOutput,
 
diff --git a/espanso/src/gui/modulo/mod.rs b/espanso/src/gui/modulo/mod.rs
index 9e27396..e065ab4 100644
--- a/espanso/src/gui/modulo/mod.rs
+++ b/espanso/src/gui/modulo/mod.rs
@@ -20,3 +20,4 @@
 pub mod form;
 pub mod manager;
 pub mod search;
+pub mod textview;
diff --git a/espanso/src/gui/modulo/textview.rs b/espanso/src/gui/modulo/textview.rs
new file mode 100644
index 0000000..66d0834
--- /dev/null
+++ b/espanso/src/gui/modulo/textview.rs
@@ -0,0 +1,51 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use crate::gui::TextUI;
+
+use super::manager::ModuloManager;
+
+pub struct ModuloTextUI<'a> {
+  manager: &'a ModuloManager,
+}
+
+impl<'a> ModuloTextUI<'a> {
+  pub fn new(manager: &'a ModuloManager) -> Self {
+    Self { manager }
+  }
+}
+
+impl<'a> TextUI for ModuloTextUI<'a> {
+  fn show_text(&self, title: &str, text: &str) -> anyhow::Result<()> {
+    self
+      .manager
+      .invoke_no_output(&["textview", "--title", title, "-i", "-"], text)?;
+
+    Ok(())
+  }
+
+  fn show_file(&self, title: &str, path: &std::path::Path) -> anyhow::Result<()> {
+    let path_str = path.to_string_lossy().to_string();
+    self
+      .manager
+      .invoke_no_output(&["textview", "--title", title, "-i", &path_str], "")?;
+
+    Ok(())
+  }
+}