{"id":19297152,"url":"https://github.com/smwhr/tinta","last_synced_at":"2026-02-06T18:48:08.170Z","repository":{"id":199738210,"uuid":"703264613","full_name":"smwhr/tinta","owner":"smwhr","description":"Lua port of inkle's ink, a scripting language for writing interactive narrative","archived":false,"fork":false,"pushed_at":"2024-10-16T07:39:30.000Z","size":169,"stargazers_count":28,"open_issues_count":2,"forks_count":4,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-04-22T08:43:33.393Z","etag":null,"topics":["ink","inkle","inky"],"latest_commit_sha":null,"homepage":"","language":"Lua","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/smwhr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-10-10T23:17:46.000Z","updated_at":"2025-04-21T19:17:13.000Z","dependencies_parsed_at":"2023-11-13T14:55:24.913Z","dependency_job_id":"e32f260c-c9f9-4520-852e-250bf7446987","html_url":"https://github.com/smwhr/tinta","commit_stats":null,"previous_names":["smwhr/tinta"],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/smwhr/tinta","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smwhr%2Ftinta","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smwhr%2Ftinta/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smwhr%2Ftinta/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smwhr%2Ftinta/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/smwhr","download_url":"https://codeload.github.com/smwhr/tinta/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smwhr%2Ftinta/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29172625,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-06T16:33:35.550Z","status":"ssl_error","status_checked_at":"2026-02-06T16:33:30.716Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ink","inkle","inky"],"created_at":"2024-11-09T23:01:18.518Z","updated_at":"2026-02-06T18:48:08.141Z","avatar_url":"https://github.com/smwhr.png","language":"Lua","funding_links":[],"categories":["Lua"],"sub_categories":[],"readme":"# Tinta\n\nThis is a lua port of inkle's [ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative.\n\ntinta is fully compatible with the language (see missing features below for what's missing in the engine), has zero dependency and is known to work with love2d and the playdate sdk.\n\n## Installation\n\nClone this repository and add the `tinta/source` directory to your project as `tinta`.\n\n## Writing and compiling Ink\n\ntinta only implements a _runtime_ for ink, you will need to use a third party compiler (the original inklecate or the inkjs compiler) to compile your ink files to json. \n\n## Running your ink\n\nFor performance reasons, tinta is not able to run the compiled json files directly. Instead, you will need to convert the json to lua using the provided `json_to_lua.sh` or `json_to_lua.ps1` command line tool.\n\n```sh\njson_to_lua.sh my_story.json my_story.lua\n```\n\nNote that you might need to change the script execution policy if you want to run the ps1 script.\n\n```\njson_to_lua.ps1 my_story.json my_story.lua\n```\n\nOnce converted, you can `import` your story and run it.\n\n```lua\nlocal storyDefinition = import(\"my_story\")\n\nStory = import('tinta/engine/story')\nstory = Story(storyDefinition)\n```\n\n2 examples loop to run the story are provided in the `run.lua` file\n\nA simple synchronous version:\n```lua\n    --- SIMPLE SYNC VERSION\n    while story:canContinue() do\n        local t = story:Continue()\n        io.write(t)\n        local tags = story:currentTags()\n        if  #tags \u003e 0 then\n            io.write(\" # tags: \" .. table.concat(tags, \", \"), '\\n')\n        end\n    end\n```\n\nA more complex asynchronous version for limited environments (like on the playdate):\n\n```lua\n    --- ASYNC VERSION\n    local textBuffer = {}\n    repeat\n        if not story:canContinue() then\n            break\n        end\n        story:ContinueAsync(300)\n        if story:asyncContinueComplete() then\n            local currentText = story:currentText()\n            local currentTags = story:currentTags()\n            table.insert(textBuffer,{\n                text = currentText,\n                tags = currentTags\n            })\n        end\n    until not story:canContinue()\n```\n### Saving and Loading\n\nSaving would return a lua table representing the current state of the story.\n\n```lua\nlocal saveData = story.state:save()\n\n-- if on playdate\nplaydate.datastore.write(saveData)\n```\n\nLoading overwrites the current state of the story with the saved data\n\n```lua\n--if on playdate\nlocal saveData = playdate.datastore.read()\n\nstory.state:load(saveData)\n```\n\n### Variable Observers\n\nVariable Observers are functions that get called whenever a variable declared in ink is changed.\n\nYou can add a variable observer like this:\n\n```lua\n-- Anonymous function (if you don't remove them later)\nstory:ObserveVariable(\"myVarDeclaredInInk\", function(varName, val) \n        -- do stuff\n\t\tprint(varName..\" changed to \".. tostring(val))\nend)\n\n\n-- Named function\nlocal function MyVarObserver(varName, val)\n    -- do stuff\n    print(varName..\" changed to \".. tostring(val))\nend\nstory:ObserveVariable(\"myVarDeclaredInInk\", MyVarObserver)\n```\n\nNote that variable observers are identified by function addresses, adding the same observer multiple times is the same as adding it only once.\n\nAdditionally, you could add the same observer to multiple variables:\n\n```lua\n-- Anonymous function (if you don't remove them later)\nstory:ObserveVariables({ \"myVarDeclaredInInk1\", \"myVarDeclaredInInk2\" }, function(varName, val) \n        -- do stuff\n\t\tprint(varName..\" changed to \".. tostring(val))\nend)\n```\n\nRemoving variable observers:\n\n```lua\n-- Remove all observers on myVarDeclaredInInk\nstory:RemoveVariableObserver(nil, \"myVarDeclaredInInk\")\n\n-- Remove a specific observer on all variables\nstory:RemoveVariableObserver(MyVarObserver, nil)\n\n-- Remove a specific observer on myVarDeclaredInInk\nstory:RemoveVariableObserver(MyVarObserver, \"myVarDeclaredInInk\")\n```\n\n### External Functions\n\nExternal Functions are lua functions that can be called from ink. \n\nBinding External Functions:\n\n```lua\nlocal MyFunc(args)\n    -- do stuff\nend\n\n-- by default, external functions are not look ahead safe.\nstory:BindExternalFunction(\"functionNameDeclaredInInk\", MyFunc)\n\n-- if your function is look ahead safe:\nstory:BindExternalFunction(\"functionNameDeclaredInInk\", MyFunc, true)\n```\n\nFallbacks are enabled by default. Which means if an external function is called but none has been bound, we call the fallback function defined in ink.\n\nThe first `Continue()` call will validate all external function bindings. If you forgot to bind an external function while the function has no fallback (or fallback is disabled), it throws an error. \n\n**You can't bind multiple functions to the same external function declaration.** If you try to do so, it throws an error.\n\n\n\n**Note that external function only receives a table as its first argument.** If you declare your function in ink like this:\n\n```ink\nEXTERNAL someFunction(argumentA, argumentB)\n```\n\nYour lua function should be:\n\n```lua\nfunction someFunction(args)\n    local argumentA = args[1]\n    local argumentB = args[2]\n    -- do something with argumentA and argumentB\nend\n```\n\nYou could remove bindings, but in that case you should have a fallback defined in ink or bind with another lua function immediately afterwards. Otherwise calling that function would result in error.\n\n```lua\nstory:UnbindExternalFunction(\"functionNameDeclaredInInk\")\n```\n\nUnbinding a function that hasn't been bound throws an error. \n\nYou could check if a function has been bound by using the following:\n\n```lua\n-- returns nil if the function hasn't been bound. \nlocal externalFunc = story:TryGetExternalFunction(\"functionNameDeclaredInInk\")\n```\n\n### Flows\n\nFlows exist even if you don't use them. Every story has a default flow named `DEFAULT_FLOW`. \n\nSome nottable getters:\n\n```lua\nstory:currentFlowName()\n\n-- true if current flow is \"DEFAULT_FLOW\"\nstory:currentFlowIsDefaultFlow()\n\nstory:aliveFlowNames()\n```\n\nTo create or switch to a named flow:\n\n```lua\nstory:SwitchFlow(\"MyFlow\")\n-- Convenient function to switch to default flow\nstory:SwitchToDefaultFlow()\n```\n\nThis will create a new flow if that flow is not found. When this happens, the newly created flow doesn't know where it should be, and calling `Continue` would not advance the story, thus you must use `story:ChoosePathString(...)` to specify where that flow should start.\n\nNote that, though temporary variables and callstacks are flow-specific, global variables are shared between flows.\n\nYou could remove a flow:\n\n```lua\nstory:RemoveFlow(\"MyFlow\")\n```\n\nYou can't remove the default flow.\n\n\n\n## CLI Player\n\nA fancy one-liner to run your story from the command line. From the `source` folder of the repository, run:\n\n```sh\nTMPSUFFIX=.lua; lua run.lua =(../json_to_lua.sh /path/to/your/game/my_story.ink.json \u003e(cat ))\n```\n\nUseful commands when prompted for input are:\n\n- `save` to save the current state of the story\n- `load` to load the last saved state\n- `-\u003e your_knot` to jump to a specific knot\n- `quit` or `q` to quit the story\n\n\n## Toybox\n\ntinta is also available using the [toybox](https://pypi.org/project/toyboxpy/) package manager. \n\n```sh\npip install toyboxpy\ntoybox add smwhr/tinta\n```\n\nthen in your lua code:\n\n```lua\nimport \"../toyboxes/toyboxes\"\n\nlocal my_story = import(\"my_story\")\nlocal story = Story(my_story)\n```\n\n## Löve2D\n\nDownload the full source code and copy the `source` folder inside your Löve2D game directory.  \nRename this folder `tinta`.\n\nthen in your lua code:\n\n```\nStory = require(\"tinta/love\")\n\nlocal my_story = import(\"my_story\")\nlocal story = Story(my_story)\n```\n\n## Picotron\n*(documentation in progress)*\n\n## Notably missing features\n\n- Global event broadcasts (e.g. whenever story continues)\n\nFeel free to contribute to the project if you need any of these features.  \nThe lua code is a straight port of the original ink code, so it should be easy to port missing features.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmwhr%2Ftinta","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmwhr%2Ftinta","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmwhr%2Ftinta/lists"}