themakunga/tennant.nvim

a Neovim plugin that converts text to speech using each operating system's native TTS engine: say on macOS, PowerShell on Windows, and spd-say / espeak-ng / espeak / festival on Linux. No external Lua dependencies or paid APIs required.

Lua

1

32 commits

updated Sep 28, 2026

See the code

See what people are saying

SourceMessageScoreDate

Tennant.nvim a tts helper for neovim (r/neovim)

Hi. I’m currently developing a plugin to help me develop using TTS. Right now only support English and Spanish (my native language). Use the buildin SAY in Mac OS and ESPEAK in Linux. Even as you can see in the repo I have automated test for power shell. So. Now I’m open to issues. Comentarios and…

1

Sep 30, 2026

README

tennant.nvim

CI Neovim 0.12+ macOS · Linux · Windows MIT


English

What is it?

tennant.nvim is a Neovim plugin that converts text to speech using each operating system's native TTS engine: say on macOS, PowerShell on Windows, and spd-say / espeak-ng / espeak / festival on Linux. No external Lua dependencies or paid APIs required.

Requirements

PlatformRequirement
macOSsay (built-in)
Linuxspd-say, espeak-ng, espeak or festival (any one)
WindowsPowerShell + .NET Framework (built-in)
Neovim>= 0.12
TreesitterOptional — improves block reading by type

Installation

vim.pack.add (Neovim >= 0.12)

Add this to your init.lua:

vim.pack.add({ 'https://github.com/themakunga/tennant.nvim' })
require('tennant').setup()
{
  'themakunga/tennant.nvim',
  event = 'VeryLazy',
  opts = {
    -- prefix = '<leader>tv', -- default prefix
  },
}

packer.nvim

use {
  'themakunga/tennant.nvim',
  config = function()
    require('tennant').setup()
  end,
}

vim-plug

Plug 'themakunga/tennant.nvim'

Then in your config:

require('tennant').setup()

vim packages (:h packages)

mkdir -p ~/.local/share/nvim/site/pack/plugins/start
git clone https://github.com/themakunga/tennant.nvim \
  ~/.local/share/nvim/site/pack/plugins/start/tennant.nvim

Add to your init.lua:

require('tennant').setup()

rocks.nvim

# in your rocks.toml
[plugins]
"tennant.nvim" = "scm"

Or in your config:

require('rocks').install('tennant.nvim')

Configuration

require('tennant').setup({
  prefix = '<leader>tv',
  language = 'en', -- 'es' (default) or 'en'
  tts = {
    -- backend = 'espeak-ng', -- optional; choose an installed engine
    -- voice = 'en', -- voice name for the chosen engine
    -- rate = 190, -- units and range depend on the engine
  },
})

Set language = 'en' for English announcements, spoken labels, and command/keymap descriptions. Spanish ('es') remains the default. File contents and external notifications are read without translation. tts.voice selects an installed voice; language does not automatically change it. See the options table at the end.

Keymaps

With the default prefix <leader>tv:

KeyAction
<leader>tvwRead word under cursor
<leader>tvlRead current line
<leader>tvl (Visual mode)Read selected text (v, V, or Ctrl-v)
<leader>tvbRead the full nearest block (uses Treesitter if available)
<leader>tvpMove up and read the enclosing block
<leader>tvnRead notifications
<leader>tvsStop speaking

Commands

CommandDescription
:TennantWordRead word under cursor
:[range]TennantLineRead current line or the given range (for example, :2,5TennantLine)
:TennantBlockRead the full nearest block
:TennantParentMove up and read the enclosing block
:TennantNotifyRead notifications
:TennantStopStop current speech

<leader>tvb starts at the nearest block to the cursor. Each <leader>tvp moves up one level through the whole file, then announces “No more nesting levels” when configured in English. Moving the cursor or editing text restarts traversal from the current position. Without a Treesitter parser, reading falls back to the current line.

Reading notifications

<leader>tvn or :TennantNotify opens a temporary window, announces the number of notifications captured during the session, and reads the first in arrival order. Press n for the next notification or x to cancel, stop speech, and close the window. Esc, :TennantStop, and <leader>tvs also cancel. The last notification announces the end; x closes the window. Your usual mappings remain intact outside it. New arrivals are included when starting another traversal; history is retained until Neovim exits.

Treesitter block types

When Treesitter is available, :TennantBlock identifies and announces the block type: function, method, arrow function, variable, constant, class, if block, for loop, while loop, try block, return, import, export.


Español

¿Qué es?

tennant.nvim es un plugin de Neovim que convierte texto en voz usando el motor TTS nativo de cada sistema operativo: say en macOS, PowerShell en Windows y spd-say / espeak-ng / espeak / festival en Linux. No requiere dependencias externas de Lua ni APIs de pago.

Requisitos

PlataformaRequisito
macOSsay (incluido en el sistema)
Linuxspd-say, espeak-ng, espeak o festival (cualquiera)
WindowsPowerShell + .NET Framework (incluido en Windows)
Neovim>= 0.12
TreesitterOpcional — mejora la lectura de bloques por tipo

Instalación

vim.pack.add (Neovim >= 0.12)

Agrega esto a tu init.lua:

vim.pack.add({ 'https://github.com/themakunga/tennant.nvim' })
require('tennant').setup()

lazy.nvim (recomendado)

{
  'themakunga/tennant.nvim',
  event = 'VeryLazy',
  opts = {
    -- prefix = '<leader>tv', -- prefijo por defecto
  },
}

packer.nvim

use {
  'themakunga/tennant.nvim',
  config = function()
    require('tennant').setup()
  end,
}

vim-plug

Plug 'themakunga/tennant.nvim'

Luego en tu config:

require('tennant').setup()

vim packages (:h packages)

mkdir -p ~/.local/share/nvim/site/pack/plugins/start
git clone https://github.com/themakunga/tennant.nvim \
  ~/.local/share/nvim/site/pack/plugins/start/tennant.nvim

Agrega en tu init.lua:

require('tennant').setup()

rocks.nvim

# en tu rocks.toml
[plugins]
"tennant.nvim" = "scm"

O en tu config:

require('rocks').install('tennant.nvim')

Configuración

require('tennant').setup({
  prefix = '<leader>tv',
  language = 'es', -- 'es' (predeterminado) o 'en'
  tts = {
    -- backend = 'espeak-ng', -- opcional; fuerza un motor instalado
    -- voice = 'es', -- nombre de voz del motor seleccionado
    -- rate = 190, -- unidad y rango dependen del motor
  },
})

language cambia los avisos, las etiquetas leídas y las descripciones de comandos y atajos. El contenido del archivo y las notificaciones externas se leen sin traducir. tts.voice selecciona una voz instalada; language no cambia la voz automáticamente. Consulta la tabla de opciones al final.

Atajos de teclado

Con el prefijo por defecto <leader>tv:

AtajoAcción
<leader>tvwLeer la palabra bajo el cursor
<leader>tvlLeer la línea actual
<leader>tvl (modo visual)Leer el texto seleccionado (v, V o Ctrl-v)
<leader>tvbLeer el bloque completo más cercano (usa Treesitter si está disponible)
<leader>tvpSubir y leer el bloque contenedor
<leader>tvnLeer las notificaciones
<leader>tvsDetener la lectura

Comandos

ComandoDescripción
:TennantWordLee la palabra bajo el cursor
:[rango]TennantLineLee la línea actual o el rango indicado (por ejemplo, :2,5TennantLine)
:TennantBlockLee el bloque completo más cercano
:TennantParentSube y lee el bloque contenedor
:TennantNotifyLee las notificaciones
:TennantStopDetiene la lectura en curso

<leader>tvb inicia la lectura desde el bloque más cercano al cursor. Cada <leader>tvp sube un nivel hasta leer el archivo; después anuncia «No existen más niveles de anidación». Mover el cursor o editar el texto reinicia el recorrido desde la posición actual. Sin un parser de Treesitter, se lee la línea actual.

Lectura de notificaciones

<leader>tvn o :TennantNotify abre una ventana temporal, anuncia el total de notificaciones capturadas durante la sesión y lee la primera, en orden de llegada. Presiona n para leer la siguiente y x para cancelar, detener la voz y cerrar la ventana. Esc, :TennantStop y <leader>tvs también cancelan. Al terminar, se anuncia que no hay más notificaciones; x cierra la ventana. Tus atajos habituales se conservan fuera de ella. Las notificaciones nuevas se incluyen al iniciar otro recorrido; el historial se conserva hasta cerrar Neovim.

Bloques reconocidos por Treesitter

Cuando Treesitter está disponible, :TennantBlock identifica y anuncia el tipo de bloque: función, método, función flecha, variable, constante, clase, bloque if, bucle for, bucle while, bloque try, retorno, importación, exportación.


Development / Desarrollo

See CONTRIBUTING.md for hooks, tests, branch policy and daily pre-releases. Consulta esa guía para contribuir mediante PR hacia main. Security reports / Reportes de seguridad: SECURITY.md.

License

MIT

Options / Opciones

require('tennant').setup({ ... }) accepts / acepta:

Option / OpciónDefault / PredeterminadoDescription / Descripción
prefix'<leader>tv'Keymap prefix / Prefijo de atajos.
language'es'Announcements and labels: 'es' or 'en' / Avisos y etiquetas: 'es' o 'en'.
tts.backendAuto-detected / Autodetectado'say' (macOS), 'powershell' (Windows), 'spd-say', 'espeak-ng', 'espeak' or / o 'festival' (Linux). The chosen engine must be available / El motor indicado debe estar disponible.
tts.voiceSystem default / Voz del sistemaName of an installed voice / Nombre de una voz instalada. Unavailable with / No disponible con festival.
tts.rateEngine default / Predeterminado del motorSpeed; units and ranges below / Velocidad; unidades y rangos en la tabla siguiente.
tts.pitchEngine default / Predeterminado del motorPitch / Tono; supported by / compatible con spd-say, espeak-ng, espeak.
tts.volumeEngine default / Predeterminado del motorVolume or amplitude / Volumen o amplitud; supported by / compatible con Windows, spd-say, espeak-ng, espeak.
Engine / Motortts.voicetts.ratetts.pitchtts.volume
macOS sayVoice from say -v '?' / Voz de say -v '?'Words per minute, integer ≥ 1 / Palabras por minuto, entero ≥ 1——
Windows PowerShellInstalled voice name / Nombre de voz instaladaInteger / Entero −10…10—Integer / Entero 0…100
Linux spd-saySynthesis voice (spd-say -L) / Voz de síntesis (spd-say -L)Integer / Entero −100…100Integer / Entero −100…100Integer / Entero −100…100
Linux espeak-ng, espeakVoice from --voices / Voz de --voicesWords per minute, integer ≥ 1 / Palabras por minuto, entero ≥ 1Integer / Entero 0…99Amplitude, integer / Amplitud, entero 0…200
Linux festival————

English: On Linux, the first available engine is selected in this order: spd-say, espeak-ng, espeak, festival. Use tts.backend to choose another. All tts options are optional; an out-of-range value, unavailable engine, or option unsupported by the chosen engine raises a configuration error. Voice names and speed units depend on the engine. Example: tts = { backend = 'espeak-ng', voice = 'en', rate = 190, pitch = 55 }.

Español: En Linux, el primer motor disponible se elige en este orden: spd-say, espeak-ng, espeak, festival. Usa tts.backend para elegir otro. Todas las opciones de tts son opcionales; un valor fuera de rango, un motor inexistente o una opción que no admita el motor produce un error de configuración. El nombre de voz y las unidades de velocidad dependen del motor. Ejemplo: tts = { backend = 'espeak-ng', voice = 'es', rate = 190, pitch = 55 }.

themakunga/tennant.nvim

a Neovim plugin that converts text to speech using each operating system's native TTS engine: say on macOS, PowerShell on Windows, and spd-say / espeak-ng / espeak / festival on Linux. No external Lua dependencies or paid APIs required.

Lua

1

32 commits

updated Sep 28, 2026

See the code

See what people are saying

SourceMessageScoreDate

Tennant.nvim a tts helper for neovim (r/neovim)

Hi. I’m currently developing a plugin to help me develop using TTS. Right now only support English and Spanish (my native language). Use the buildin SAY in Mac OS and ESPEAK in Linux. Even as you can see in the repo I have automated test for power shell. So. Now I’m open to issues. Comentarios and…

1

Sep 30, 2026

README

tennant.nvim

CI Neovim 0.12+ macOS · Linux · Windows MIT


English

What is it?

tennant.nvim is a Neovim plugin that converts text to speech using each operating system's native TTS engine: say on macOS, PowerShell on Windows, and spd-say / espeak-ng / espeak / festival on Linux. No external Lua dependencies or paid APIs required.

Requirements

PlatformRequirement
macOSsay (built-in)
Linuxspd-say, espeak-ng, espeak or festival (any one)
WindowsPowerShell + .NET Framework (built-in)
Neovim>= 0.12
TreesitterOptional — improves block reading by type

Installation

vim.pack.add (Neovim >= 0.12)

Add this to your init.lua:

vim.pack.add({ 'https://github.com/themakunga/tennant.nvim' })
require('tennant').setup()
{
  'themakunga/tennant.nvim',
  event = 'VeryLazy',
  opts = {
    -- prefix = '<leader>tv', -- default prefix
  },
}

packer.nvim

use {
  'themakunga/tennant.nvim',
  config = function()
    require('tennant').setup()
  end,
}

vim-plug

Plug 'themakunga/tennant.nvim'

Then in your config:

require('tennant').setup()

vim packages (:h packages)

mkdir -p ~/.local/share/nvim/site/pack/plugins/start
git clone https://github.com/themakunga/tennant.nvim \
  ~/.local/share/nvim/site/pack/plugins/start/tennant.nvim

Add to your init.lua:

require('tennant').setup()

rocks.nvim

# in your rocks.toml
[plugins]
"tennant.nvim" = "scm"

Or in your config:

require('rocks').install('tennant.nvim')

Configuration

require('tennant').setup({
  prefix = '<leader>tv',
  language = 'en', -- 'es' (default) or 'en'
  tts = {
    -- backend = 'espeak-ng', -- optional; choose an installed engine
    -- voice = 'en', -- voice name for the chosen engine
    -- rate = 190, -- units and range depend on the engine
  },
})

Set language = 'en' for English announcements, spoken labels, and command/keymap descriptions. Spanish ('es') remains the default. File contents and external notifications are read without translation. tts.voice selects an installed voice; language does not automatically change it. See the options table at the end.

Keymaps

With the default prefix <leader>tv:

KeyAction
<leader>tvwRead word under cursor
<leader>tvlRead current line
<leader>tvl (Visual mode)Read selected text (v, V, or Ctrl-v)
<leader>tvbRead the full nearest block (uses Treesitter if available)
<leader>tvpMove up and read the enclosing block
<leader>tvnRead notifications
<leader>tvsStop speaking

Commands

CommandDescription
:TennantWordRead word under cursor
:[range]TennantLineRead current line or the given range (for example, :2,5TennantLine)
:TennantBlockRead the full nearest block
:TennantParentMove up and read the enclosing block
:TennantNotifyRead notifications
:TennantStopStop current speech

<leader>tvb starts at the nearest block to the cursor. Each <leader>tvp moves up one level through the whole file, then announces “No more nesting levels” when configured in English. Moving the cursor or editing text restarts traversal from the current position. Without a Treesitter parser, reading falls back to the current line.

Reading notifications

<leader>tvn or :TennantNotify opens a temporary window, announces the number of notifications captured during the session, and reads the first in arrival order. Press n for the next notification or x to cancel, stop speech, and close the window. Esc, :TennantStop, and <leader>tvs also cancel. The last notification announces the end; x closes the window. Your usual mappings remain intact outside it. New arrivals are included when starting another traversal; history is retained until Neovim exits.

Treesitter block types

When Treesitter is available, :TennantBlock identifies and announces the block type: function, method, arrow function, variable, constant, class, if block, for loop, while loop, try block, return, import, export.


Español

¿Qué es?

tennant.nvim es un plugin de Neovim que convierte texto en voz usando el motor TTS nativo de cada sistema operativo: say en macOS, PowerShell en Windows y spd-say / espeak-ng / espeak / festival en Linux. No requiere dependencias externas de Lua ni APIs de pago.

Requisitos

PlataformaRequisito
macOSsay (incluido en el sistema)
Linuxspd-say, espeak-ng, espeak o festival (cualquiera)
WindowsPowerShell + .NET Framework (incluido en Windows)
Neovim>= 0.12
TreesitterOpcional — mejora la lectura de bloques por tipo

Instalación

vim.pack.add (Neovim >= 0.12)

Agrega esto a tu init.lua:

vim.pack.add({ 'https://github.com/themakunga/tennant.nvim' })
require('tennant').setup()

lazy.nvim (recomendado)

{
  'themakunga/tennant.nvim',
  event = 'VeryLazy',
  opts = {
    -- prefix = '<leader>tv', -- prefijo por defecto
  },
}

packer.nvim

use {
  'themakunga/tennant.nvim',
  config = function()
    require('tennant').setup()
  end,
}

vim-plug

Plug 'themakunga/tennant.nvim'

Luego en tu config:

require('tennant').setup()

vim packages (:h packages)

mkdir -p ~/.local/share/nvim/site/pack/plugins/start
git clone https://github.com/themakunga/tennant.nvim \
  ~/.local/share/nvim/site/pack/plugins/start/tennant.nvim

Agrega en tu init.lua:

require('tennant').setup()

rocks.nvim

# en tu rocks.toml
[plugins]
"tennant.nvim" = "scm"

O en tu config:

require('rocks').install('tennant.nvim')

Configuración

require('tennant').setup({
  prefix = '<leader>tv',
  language = 'es', -- 'es' (predeterminado) o 'en'
  tts = {
    -- backend = 'espeak-ng', -- opcional; fuerza un motor instalado
    -- voice = 'es', -- nombre de voz del motor seleccionado
    -- rate = 190, -- unidad y rango dependen del motor
  },
})

language cambia los avisos, las etiquetas leídas y las descripciones de comandos y atajos. El contenido del archivo y las notificaciones externas se leen sin traducir. tts.voice selecciona una voz instalada; language no cambia la voz automáticamente. Consulta la tabla de opciones al final.

Atajos de teclado

Con el prefijo por defecto <leader>tv:

AtajoAcción
<leader>tvwLeer la palabra bajo el cursor
<leader>tvlLeer la línea actual
<leader>tvl (modo visual)Leer el texto seleccionado (v, V o Ctrl-v)
<leader>tvbLeer el bloque completo más cercano (usa Treesitter si está disponible)
<leader>tvpSubir y leer el bloque contenedor
<leader>tvnLeer las notificaciones
<leader>tvsDetener la lectura

Comandos

ComandoDescripción
:TennantWordLee la palabra bajo el cursor
:[rango]TennantLineLee la línea actual o el rango indicado (por ejemplo, :2,5TennantLine)
:TennantBlockLee el bloque completo más cercano
:TennantParentSube y lee el bloque contenedor
:TennantNotifyLee las notificaciones
:TennantStopDetiene la lectura en curso

<leader>tvb inicia la lectura desde el bloque más cercano al cursor. Cada <leader>tvp sube un nivel hasta leer el archivo; después anuncia «No existen más niveles de anidación». Mover el cursor o editar el texto reinicia el recorrido desde la posición actual. Sin un parser de Treesitter, se lee la línea actual.

Lectura de notificaciones

<leader>tvn o :TennantNotify abre una ventana temporal, anuncia el total de notificaciones capturadas durante la sesión y lee la primera, en orden de llegada. Presiona n para leer la siguiente y x para cancelar, detener la voz y cerrar la ventana. Esc, :TennantStop y <leader>tvs también cancelan. Al terminar, se anuncia que no hay más notificaciones; x cierra la ventana. Tus atajos habituales se conservan fuera de ella. Las notificaciones nuevas se incluyen al iniciar otro recorrido; el historial se conserva hasta cerrar Neovim.

Bloques reconocidos por Treesitter

Cuando Treesitter está disponible, :TennantBlock identifica y anuncia el tipo de bloque: función, método, función flecha, variable, constante, clase, bloque if, bucle for, bucle while, bloque try, retorno, importación, exportación.


Development / Desarrollo

See CONTRIBUTING.md for hooks, tests, branch policy and daily pre-releases. Consulta esa guía para contribuir mediante PR hacia main. Security reports / Reportes de seguridad: SECURITY.md.

License

MIT

Options / Opciones

require('tennant').setup({ ... }) accepts / acepta:

Option / OpciónDefault / PredeterminadoDescription / Descripción
prefix'<leader>tv'Keymap prefix / Prefijo de atajos.
language'es'Announcements and labels: 'es' or 'en' / Avisos y etiquetas: 'es' o 'en'.
tts.backendAuto-detected / Autodetectado'say' (macOS), 'powershell' (Windows), 'spd-say', 'espeak-ng', 'espeak' or / o 'festival' (Linux). The chosen engine must be available / El motor indicado debe estar disponible.
tts.voiceSystem default / Voz del sistemaName of an installed voice / Nombre de una voz instalada. Unavailable with / No disponible con festival.
tts.rateEngine default / Predeterminado del motorSpeed; units and ranges below / Velocidad; unidades y rangos en la tabla siguiente.
tts.pitchEngine default / Predeterminado del motorPitch / Tono; supported by / compatible con spd-say, espeak-ng, espeak.
tts.volumeEngine default / Predeterminado del motorVolume or amplitude / Volumen o amplitud; supported by / compatible con Windows, spd-say, espeak-ng, espeak.
Engine / Motortts.voicetts.ratetts.pitchtts.volume
macOS sayVoice from say -v '?' / Voz de say -v '?'Words per minute, integer ≥ 1 / Palabras por minuto, entero ≥ 1——
Windows PowerShellInstalled voice name / Nombre de voz instaladaInteger / Entero −10…10—Integer / Entero 0…100
Linux spd-saySynthesis voice (spd-say -L) / Voz de síntesis (spd-say -L)Integer / Entero −100…100Integer / Entero −100…100Integer / Entero −100…100
Linux espeak-ng, espeakVoice from --voices / Voz de --voicesWords per minute, integer ≥ 1 / Palabras por minuto, entero ≥ 1Integer / Entero 0…99Amplitude, integer / Amplitud, entero 0…200
Linux festival————

English: On Linux, the first available engine is selected in this order: spd-say, espeak-ng, espeak, festival. Use tts.backend to choose another. All tts options are optional; an out-of-range value, unavailable engine, or option unsupported by the chosen engine raises a configuration error. Voice names and speed units depend on the engine. Example: tts = { backend = 'espeak-ng', voice = 'en', rate = 190, pitch = 55 }.

Español: En Linux, el primer motor disponible se elige en este orden: spd-say, espeak-ng, espeak, festival. Usa tts.backend para elegir otro. Todas las opciones de tts son opcionales; un valor fuera de rango, un motor inexistente o una opción que no admita el motor produce un error de configuración. El nombre de voz y las unidades de velocidad dependen del motor. Ejemplo: tts = { backend = 'espeak-ng', voice = 'es', rate = 190, pitch = 55 }.

Languages

Lua

73.3%

Python

26.7%