TNF (Terminal Novell Framework) is an engine for creating interactive text visual novels that run directly in your terminal.
Shows ASCII art, plays music, supports choices, Lua logic, and saves games — all in a minimalistic CLI style.
Python 3.11+
pip install -r requirements.txtsource path.shNow you have convenient commands:
tf-novell run folder . --from 1
scene-ide./
├── .nvlrc # Visual novel config
├── scenes/ # Scene JSON files (1.json, 2.json, ...)
├── images/ # Background images
├── music/ # Music files
├── scripts/ # Lua scripts for scene logic
└── save.json # Save file
nvl-name=My First Novel
scene-dir=scenes
save-file=save.json| Key | Description |
|---|---|
nvl-name |
Name of the novel |
scene-dir |
Scene folder |
save-file |
Save file path |
Every scene is a .json file:
{
"id": 1,
"text": "Hi, this is my first scene!",
"person": "Main Character",
"background": "images/room.png",
"music": "music/theme.mp3",
"script": "scripts/scene1.lua"
}| Field | Type | Description |
|---|---|---|
id |
int | Scene number |
text |
str | Scene text |
person |
str | Speaker’s name |
background |
str | Path to background image |
| Field | Type | Description |
|---|---|---|
music |
str | Path to music file |
script |
str | Lua logic script |
The engine has integrated Lua 5.1 via lupa. You can add custom logic to scenes.
function modify_scene(scene)
-- Called BEFORE the scene is shown
return scene
end
function post_scene(scene)
-- Called AFTER the scene is shown
return scene
end| Method | Args | Returns | Description |
|---|---|---|---|
engine.get_scene() |
– | table |
Gets current scene as a Lua table |
engine.load_scene(scene, exec) |
scene, execute? (bool) |
– | Loads a scene and applies logic |
engine.next_scene() |
– | – | Next scene |
engine.prev_scene() |
– | – | Previous scene |
engine.custom_scene(id) |
id (int) |
– | Load a scene by ID |
engine.apply_lua_logic(name) |
name (str, default modify_scene) |
– | Runs a Lua function by name |
| Method | Args | Returns | Description |
|---|---|---|---|
engine.add_choice(name, txt) |
name (str), txt (str) |
– | Adds a choice |
engine.get_choice(name) |
name (str) |
str |
Gets the text of a choice |
engine.delete_choice(name) |
name (str) |
– | Deletes a choice |
| Method | Args | Returns | Description |
|---|---|---|---|
engine.play_audio(path) |
file_path |
– | Plays music |
engine.stop_audio() |
– | – | Stops music playback |
| Method | Args | Returns | Description |
|---|---|---|---|
engine.save_game(name?) |
filename (str,opt) |
– | Saves to file (default: save.json) |
engine.load_game(name?) |
filename (str,opt) |
– | Loads the save file |
| Method | Args | Returns | Description |
|---|---|---|---|
engine.render_tab() |
– | – | Draws the bottom panel |
engine.render_scene() |
– | – | Renders ASCII background |
engine.render() |
– | – | Full scene rendering |
| Method | Args | Returns | Description |
|---|---|---|---|
engine.console.clear() |
– | – | Clear the terminal |
engine.console.print(text) |
text (str) |
– | Print formatted text |
engine.run()function modify_scene(scene)
scene.text = "This text is replaced"
return scene
endfunction post_scene(scene)
io.write("Enter a number: ")
engine.add_choice("choice_rand", io.read())
engine.await_input = false
return scene
endfunction modify_scene(scene)
local last = engine.get_choice("choice_rand")
if last == "42" then
scene.text = "You picked 42! Awesome choice!"
else
scene.text = "Strange choice... but OK."
end
return scene
endtf-novell validateChecks project structure and file presence.
tf-novell preview --scene 2Shows the chosen scene in your terminal.
tf-novell run folder . --from 1Mini-editor for quick scene editing/creation:
scene-ide- Edits
.jsondialogue files - Visual path selection for backgrounds, music, and scripts
- Saves files in the required folder
- Always use relative paths
- Check UTF-8 encoding in your JSON scene files
- Run
validatebefore running the novel - Don’t overuse ASCII art—terminals can lag 😅
(Coming soon) Ability to build a .nvlpkg archive to distribute your novel.
If you use a script in one scene, it will persist for subsequent scenes until modify_scene or post_scene is overridden by new script files.
