├── .clang-format ├── .clang-tidy ├── .dockerignore ├── .editorconfig ├── .github ├── renovate.json └── workflows │ ├── check-demos.yml │ ├── check-workflows.yml │ ├── check.yml │ ├── maintenance.yml │ └── release.yml ├── .gitignore ├── .luarc.json ├── LICENSE.md ├── Makefile ├── README.md ├── demos ├── assets │ └── uv.ppm ├── fractal.lua ├── game-of-life.lua ├── heart.lua ├── image.lua ├── lib │ └── ppm.lua ├── moving-box.lua ├── multi-window.lua ├── noise.lua ├── paint.lua ├── plasma.lua ├── rhodena-curve.lua ├── text-editor.lua └── tunnel.lua ├── docker-compose.yml ├── fenster-dev-1.rockspec ├── include └── main.h ├── lib ├── compat-5.3 │ ├── compat-5.3.c │ └── compat-5.3.h └── fenster │ └── fenster.h ├── spec └── main_spec.lua └── src └── main.c /.clang-format: -------------------------------------------------------------------------------- 1 | # clang-format configuration 2 | # See: https://clang.llvm.org/docs/ClangFormatStyleOptions.html 3 | 4 | BasedOnStyle: Google 5 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | # clang-tidy configuration 2 | # See: https://clang.llvm.org/extra/clang-tidy/ 3 | 4 | Checks: >- 5 | -*, 6 | bugprone-*, 7 | 8 | cert-*, 9 | 10 | clang-analyzer-*, 11 | -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, 12 | 13 | google-*, 14 | 15 | misc-*, 16 | 17 | modernize-*, 18 | 19 | performance-*, 20 | 21 | portability-*, 22 | 23 | readability-*, 24 | 25 | thread-safety-*, 26 | 27 | security-*, 28 | 29 | CheckOptions: 30 | misc-header-include-cycle.IgnoredFilesList: '\/lib\/compat-5\.3\/compat-5\.3\.h$' 31 | misc-include-cleaner.IgnoreHeaders: '\/lib\/compat-5\.3\/compat-5\.3\.h$' 32 | readability-identifier-length.IgnoredVariableNames: '^[xy]$' # allow coordinate variables 33 | readability-identifier-length.IgnoredParameterNames: '^[L]$' # allow Lua state parameter 34 | 35 | FormatStyle: file # use .clang-format file 36 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Docker ignore file 2 | # See: https://docs.docker.com/build/concepts/context/#dockerignore-files 3 | 4 | # OS 5 | .DS_Store 6 | 7 | # IDE 8 | .idea/ 9 | .vscode/ 10 | 11 | # LuaRocks 12 | luarocks/ 13 | lua/ 14 | lua_modules/ 15 | .luarocks/ 16 | *.src.rock 17 | *.zip 18 | *.tar.gz 19 | 20 | # Output 21 | *.o 22 | *.so 23 | *.dll 24 | *.exp 25 | *.lib 26 | *.obj 27 | *.def 28 | *.rock 29 | 30 | # Temp Files 31 | /test.lua 32 | 33 | # Not needed in Testing Docker Container 34 | .git/ 35 | .github/ 36 | /demos/ 37 | .clang-format 38 | .clang-tidy 39 | .dockerignore 40 | .editorconfig 41 | .gitignore 42 | .luarc.json 43 | *.md 44 | Makefile 45 | docker-compose.yml 46 | docker-compose.yaml 47 | compose.yml 48 | compose.yaml 49 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig configuration 2 | # See: https://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = tab 8 | indent_size = tab 9 | #tab_width = 4 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | max_line_length = 80 15 | 16 | [*.{json,yml,yaml,md,mdx,c,h,cpp,hpp,cc,hh}] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.{json,ppm}] 21 | insert_final_newline = false 22 | 23 | [*.{md,mdx}] 24 | trim_trailing_whitespace = false 25 | 26 | [*.{lua,rockspec}] 27 | max_line_length = 120 28 | 29 | # Custom options for EmmyLuaCodeStyle 30 | # See: https://github.com/CppCXY/EmmyLuaCodeStyle/blob/master/docs/format_config_EN.md 31 | quote_style = single 32 | trailing_table_separator = smart 33 | align_function_params = false 34 | align_continuous_assign_statement = false 35 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>jonasgeiler/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/check-demos.yml: -------------------------------------------------------------------------------- 1 | name: Check Demos 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'demos/**' 9 | 10 | pull_request: 11 | branches: 12 | - main 13 | paths: 14 | - 'demos/**' 15 | 16 | workflow_dispatch: 17 | workflow_call: 18 | 19 | jobs: 20 | lint: 21 | name: Lint 22 | 23 | runs-on: ubuntu-latest 24 | defaults: 25 | run: 26 | shell: bash 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 31 | 32 | - name: Set up Lua 33 | uses: luarocks/gh-actions-lua@master 34 | 35 | - name: Set up LuaRocks 36 | uses: luarocks/gh-actions-luarocks@master 37 | 38 | - name: Run luacheck on demos 39 | env: 40 | # renovate: datasource=github-releases depName=ammaraskar/msvc-problem-matcher 41 | MSCV_PROBLEM_MATCHER_VERSION: 0.3.0 42 | run: | 43 | IFS=$' \n\t'; set -ux 44 | 45 | # install luacheck 46 | luarocks install luacheck 47 | 48 | # download and setup problem matcher for luacheck's visual_studio formatter 49 | wget -q "https://raw.githubusercontent.com/ammaraskar/msvc-problem-matcher/${MSCV_PROBLEM_MATCHER_VERSION}/msvc_matcher.json" \ 50 | -O visual-studio-problem-matcher.json 51 | echo '::add-matcher::visual-studio-problem-matcher.json' 52 | 53 | # run luacheck 54 | luacheck ./demos/ --no-cache --formatter visual_studio 55 | -------------------------------------------------------------------------------- /.github/workflows/check-workflows.yml: -------------------------------------------------------------------------------- 1 | name: Check Workflows 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'renovate/**' 8 | paths: 9 | - '.github/workflows/**.yml' 10 | 11 | pull_request: 12 | branches: 13 | - main 14 | paths: 15 | - '.github/workflows/**.yml' 16 | 17 | workflow_dispatch: 18 | workflow_call: 19 | 20 | jobs: 21 | lint: 22 | name: Lint 23 | 24 | runs-on: ubuntu-latest 25 | defaults: 26 | run: 27 | shell: bash 28 | 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 32 | 33 | - name: Run actionlint on GitHub Actions workflows 34 | env: 35 | # renovate: datasource=github-releases depName=rhysd/actionlint 36 | ACTIONLINT_VERSION: v1.7.7 37 | run: | 38 | IFS=$' \n\t'; set -ux 39 | 40 | # download, extract and setup actionlint 41 | wget -q "https://github.com/rhysd/actionlint/releases/download/${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION#v}_linux_amd64.tar.gz" \ 42 | -O actionlint.tar.gz 43 | tar -xzf actionlint.tar.gz 44 | chmod +x actionlint 45 | 46 | # download and setup problem matcher for actionlint 47 | wget -q "https://raw.githubusercontent.com/rhysd/actionlint/${ACTIONLINT_VERSION}/.github/actionlint-matcher.json" \ 48 | -O actionlint-problem-matcher.json 49 | echo '::add-matcher::actionlint-problem-matcher.json' 50 | 51 | # run actionlint 52 | ./actionlint -verbose -color 53 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'include/**' 9 | - 'lib/**' 10 | - 'spec/**' 11 | - 'src/**' 12 | - '**.rockspec' 13 | - '.clang-format' 14 | - '.clang-tidy' 15 | - 'docker-compose.yml' 16 | 17 | pull_request: 18 | branches: 19 | - main 20 | paths: 21 | - 'include/**' 22 | - 'lib/**' 23 | - 'spec/**' 24 | - 'src/**' 25 | - '**.rockspec' 26 | - '.clang-format' 27 | - '.clang-tidy' 28 | - 'docker-compose.yml' 29 | 30 | workflow_dispatch: 31 | workflow_call: 32 | 33 | jobs: 34 | lint: 35 | name: Lint 36 | 37 | runs-on: ubuntu-latest 38 | defaults: 39 | run: 40 | shell: bash 41 | 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 45 | 46 | - name: Run clang-format on C files and headers 47 | run: | 48 | IFS=$' \n\t'; set -ux 49 | 50 | # "notify" me of newer clang-format version 51 | if command -v clang-format-19 &> /dev/null; then 52 | echo 'clang-format-19 is available' 53 | exit 2 54 | fi 55 | 56 | shopt -s nullglob 57 | files=(src/*.c src/**/*.c include/*.h include/**/*.h) 58 | if [[ "${#files[@]}" -eq 0 ]]; then 59 | echo 'No files found' 60 | exit 1 61 | fi 62 | clang-format-18 --verbose --dry-run --Werror "${files[@]}" 63 | 64 | - name: Run clang-tidy on C files and headers 65 | run: | 66 | IFS=$' \n\t'; set -ux 67 | 68 | # "notify" me of newer clang-tidy version 69 | if command -v clang-tidy-19 &> /dev/null; then 70 | echo 'clang-tidy-19 is available' 71 | exit 2 72 | fi 73 | 74 | # get required header files 75 | sudo apt-get install --yes --no-install-recommends \ 76 | libx11-dev \ 77 | liblua5.1-dev \ 78 | liblua5.2-dev \ 79 | liblua5.3-dev \ 80 | liblua5.4-dev \ 81 | libluajit-5.1-dev 82 | 83 | shopt -s nullglob 84 | files=(src/*.c src/**/*.c include/*.h include/**/*.h) 85 | if [[ "${#files[@]}" -eq 0 ]]; then 86 | echo 'No files found' 87 | exit 1 88 | fi 89 | clang-tidy-18 --warnings-as-errors='*' --extra-arg='-I/usr/include/lua5.1' "${files[@]}" 90 | clang-tidy-18 --warnings-as-errors='*' --extra-arg='-I/usr/include/lua5.2' "${files[@]}" 91 | clang-tidy-18 --warnings-as-errors='*' --extra-arg='-I/usr/include/lua5.3' "${files[@]}" 92 | clang-tidy-18 --warnings-as-errors='*' --extra-arg='-I/usr/include/lua5.4' "${files[@]}" 93 | clang-tidy-18 --warnings-as-errors='*' --extra-arg='-I/usr/include/luajit-2.1' "${files[@]}" 94 | 95 | - name: Set up Lua 96 | uses: luarocks/gh-actions-lua@master # TODO: Check if leafo/gh-actions-lua is same 97 | 98 | - name: Set up LuaRocks 99 | uses: luarocks/gh-actions-luarocks@master # TODO: Check if leafo/gh-actions-luarocks is same 100 | 101 | - name: Run luacheck on rockspecs and tests 102 | env: 103 | # renovate: datasource=github-releases depName=ammaraskar/msvc-problem-matcher 104 | MSCV_PROBLEM_MATCHER_VERSION: 0.3.0 105 | run: | 106 | IFS=$' \n\t'; set -ux 107 | 108 | # install luacheck 109 | luarocks install luacheck 110 | 111 | # download and setup problem matcher for luacheck's visual_studio formatter 112 | wget -q "https://raw.githubusercontent.com/ammaraskar/msvc-problem-matcher/${MSCV_PROBLEM_MATCHER_VERSION}/msvc_matcher.json" \ 113 | -O visual-studio-problem-matcher.json 114 | echo '::add-matcher::visual-studio-problem-matcher.json' 115 | 116 | # run luacheck 117 | luacheck . --no-cache --formatter visual_studio --include-files 'spec/**/*.lua' '**/*.rockspec' --exclude-files .lua .luarocks 118 | 119 | 120 | test: 121 | name: Test 122 | needs: lint 123 | 124 | strategy: 125 | fail-fast: false 126 | matrix: 127 | os: [ ubuntu-latest, windows-latest, macos-latest, macos-13 ] 128 | lua_version: [ '5.1', '5.2', '5.3', '5.4', 'luajit-2.0', 'luajit-2.1' ] 129 | exclude: 130 | # Currently does not work for LuaJIT on Windows (Lua library not found) 131 | - os: windows-latest 132 | lua_version: 'luajit-2.0' 133 | - os: windows-latest 134 | lua_version: 'luajit-2.1' 135 | # Currently does not work for LuaJIT 2.0 on macOS with ARM64 (Architecture not supported) 136 | - os: macos-latest 137 | lua_version: 'luajit-2.0' 138 | runs-on: ${{ matrix.os }} 139 | 140 | steps: 141 | - name: Checkout 142 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 143 | 144 | - name: Set up Microsoft Visual C++ Developer Command Prompt 145 | if: matrix.os == 'windows-latest' 146 | uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 147 | 148 | - name: Set up X11 149 | if: matrix.os == 'ubuntu-latest' 150 | run: sudo apt-get install --yes --no-install-recommends libx11-dev xvfb xauth 151 | 152 | - name: Set up Lua 153 | uses: luarocks/gh-actions-lua@master 154 | with: 155 | luaVersion: ${{ matrix.lua_version }} 156 | 157 | - name: Set up LuaRocks 158 | uses: luarocks/gh-actions-luarocks@master 159 | 160 | - name: Build fenster 161 | run: luarocks make 162 | 163 | - name: Run tests 164 | run: >- 165 | ${{ 166 | matrix.os == 'ubuntu-latest' 167 | && 'trap "cat /tmp/xvfb-run-error.txt && rm --force /tmp/xvfb-run-error.txt" EXIT INT TERM QUIT; xvfb-run --auto-servernum --error-file=/tmp/xvfb-run-error.txt luarocks test -- --verbose --output TAP' 168 | || 'luarocks test -- --exclude-tags needsdisplay --verbose --output TAP' 169 | }} 170 | 171 | 172 | test-docker: 173 | name: Test (docker) 174 | needs: lint 175 | 176 | runs-on: ubuntu-latest 177 | defaults: 178 | run: 179 | shell: bash 180 | 181 | steps: 182 | - name: Checkout 183 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 184 | 185 | - name: Run Tests in Docker 186 | env: 187 | # renovate: datasource=github-releases depName=docker/compose 188 | COMPOSE_VERSION: v2.35.1 189 | run: | 190 | IFS=$' \n\t'; set -ux 191 | 192 | # Download newest version of Docker Compose plugin. 193 | compose_plugin_path="$(docker info --format='{{range .ClientInfo.Plugins}}{{if eq .Name "compose"}}{{.Path}}{{break}}{{end}}{{end}}')" 194 | sudo wget -q "https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-linux-x86_64" \ 195 | -O "${compose_plugin_path}" 196 | docker compose version 197 | 198 | # Run docker compose 199 | docker compose up --abort-on-container-failure --yes --quiet-pull 200 | -------------------------------------------------------------------------------- /.github/workflows/maintenance.yml: -------------------------------------------------------------------------------- 1 | name: Maintenance 2 | 3 | on: 4 | schedule: 5 | - cron: '0 13 1 * *' 6 | 7 | workflow_dispatch: 8 | 9 | jobs: 10 | update-libraries: 11 | name: Update Libraries 12 | 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | shell: bash 17 | 18 | permissions: 19 | contents: write 20 | pull-requests: write 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | 26 | - name: Update fenster 27 | run: wget https://raw.githubusercontent.com/zserge/fenster/main/fenster.h -O lib/fenster/fenster.h 28 | 29 | - name: Update compat-5.3 30 | run: | 31 | wget https://raw.githubusercontent.com/lunarmodules/lua-compat-5.3/master/c-api/compat-5.3.h -O lib/compat-5.3/compat-5.3.h 32 | wget https://raw.githubusercontent.com/lunarmodules/lua-compat-5.3/master/c-api/compat-5.3.c -O lib/compat-5.3/compat-5.3.c 33 | 34 | - name: Create pull request 35 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 36 | with: 37 | commit-message: 'chore(lib): update libraries' 38 | signoff: true 39 | branch: maintenance/update-libraries 40 | delete-branch: true 41 | sign-commits: true 42 | title: 'chore(lib): update libraries' 43 | body: | 44 | This PR updates the libraries used by the project. 45 | labels: libraries 46 | reviewers: ${{ github.repository_owner }} 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | check: 10 | name: Check 11 | uses: ./.github/workflows/check.yml 12 | 13 | luarocks-upload-and-github-release: 14 | name: LuaRocks Upload & GitHub Release 15 | needs: check 16 | 17 | runs-on: ubuntu-latest 18 | defaults: 19 | run: 20 | shell: bash 21 | 22 | permissions: 23 | contents: write 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | 29 | - name: Set up Lua 30 | uses: luarocks/gh-actions-lua@master 31 | 32 | - name: Set up LuaRocks 33 | uses: luarocks/gh-actions-luarocks@master 34 | 35 | - name: Modify rockspec & upload to LuaRocks server 36 | run: | 37 | IFS=$' \n\t'; set -ux 38 | 39 | version="${GITHUB_REF_NAME}" 40 | rockspec="fenster-${version#v}-1.rockspec" 41 | git mv fenster-dev-1.rockspec "${rockspec}" 42 | sed -i -E "s/\bversion\s*=\s*('|\"|\[\[)\s*[a-zA-Z0-9.-]+\s*('|\"|\]\])/version = '${version#v}-1'/" "${rockspec}" 43 | sed -i -E "s/\bbranch\s*=\s*('|\"|\[\[)\s*[a-zA-Z0-9.-_\/]+\s*('|\"|\]\])/tag = '$version'/" "${rockspec}" 44 | git diff 45 | if [[ -z "$(git status --porcelain "${rockspec}")" ]]; then 46 | echo 'Rockspec not modified' 47 | exit 1 48 | fi 49 | luarocks install dkjson 50 | luarocks upload --temp-key "${{ secrets.LUAROCKS_API_KEY }}" "${rockspec}" 51 | 52 | - name: Draft release on GitHub 53 | uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 54 | with: 55 | draft: true 56 | generate_release_notes: true 57 | fail_on_unmatched_files: true 58 | files: | 59 | fenster-*-1.rockspec 60 | fenster-*-1.src.rock 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Git ignore file 2 | # See: https://git-scm.com/docs/gitignore 3 | 4 | # OS 5 | .DS_Store 6 | 7 | # IDE 8 | .idea/ 9 | .vscode/ 10 | 11 | # LuaRocks 12 | luarocks/ 13 | lua/ 14 | lua_modules/ 15 | .luarocks/ 16 | *.src.rock 17 | *.zip 18 | *.tar.gz 19 | 20 | # Output 21 | *.o 22 | *.so 23 | *.dll 24 | *.exp 25 | *.lib 26 | *.obj 27 | *.def 28 | *.rock 29 | 30 | # Temp Files 31 | /test.lua 32 | -------------------------------------------------------------------------------- /.luarc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", 3 | "runtime.version": "LuaJIT", 4 | "codeLens.enable": true, 5 | "completion.callSnippet": "Replace", 6 | "hint.enable": true, 7 | "hint.semicolon": "Disable", 8 | "hint.setType": true, 9 | "hover.enumsLimit": 10, 10 | "runtime.pathStrict": true, 11 | "type.castNumberToInteger": false, 12 | "diagnostics.neededFileStatus": { 13 | "codestyle-check": "Any", 14 | "no-unknown": "Any" 15 | }, 16 | "diagnostics.groupSeverity": { 17 | "unused": "Information", 18 | "redefined": "Warning" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | Copyright (c) 2024 Jonas Geiler 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # !IMPORTANT! 3 | # I created this file for CLion and it works on my machine, but you should rather use `luarocks make` to build the library. 4 | # I really don't have any experience with Makefiles, so I can't guarantee that it will work on your machine. 5 | # 6 | 7 | all: fenster.so 8 | 9 | LD ?= gcc 10 | LDFLAGS ?= -shared 11 | X11_LIBDIR ?= /usr/lib64 12 | fenster.so: src/main.o 13 | $(LD) $(LDFLAGS) $(LIBFLAG) -o $@ $< -L$(X11_LIBDIR) -lX11 14 | 15 | CC ?= gcc 16 | CFLAGS ?= -O2 -fPIC 17 | LUA_INCDIR ?= /usr/include 18 | X11_INCDIR ?= /usr/include 19 | src/main.o: src/main.c 20 | $(CC) $(CFLAGS) -I$(LUA_INCDIR) -c $< -o $@ -I$(X11_INCDIR) 21 | 22 | clean: 23 | rm -f src/main.o fenster.so -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-fenster 2 | 3 | > The most minimal cross-platform GUI library - now in Lua! 4 | 5 | [![LuaRocks](https://img.shields.io/luarocks/v/jonasgeiler/fenster?style=for-the-badge&color=%232c3e67)](https://luarocks.org/modules/jonasgeiler/fenster) 6 | [![Downloads](https://img.shields.io/badge/dynamic/xml?url=https%3A%2F%2Fluarocks.org%2Fmodules%2Fjonasgeiler%2Ffenster&query=%2F%2Fdiv%5B%40class%3D%22metadata_columns_inner%22%5D%2Fdiv%5B%40class%3D%22column%22%5D%5Blast()%5D%2Ftext()&style=for-the-badge&label=Downloads&color=099dff&cacheSeconds=86400)](https://luarocks.org/modules/jonasgeiler/fenster) 7 | [![Projects using lua-fenster](https://img.shields.io/badge/2%2B-2c3e67?style=for-the-badge&label=Projects%20using%20lua-fenster)](#projects-using-lua-fenster) 8 | [![License](https://img.shields.io/github/license/jonasgeiler/lua-fenster?style=for-the-badge&color=%23099dff)](./LICENSE.md) 9 | 10 | A Lua binding for the [fenster](https://github.com/zserge/fenster) GUI library, 11 | providing the most minimal and highly opinionated way to display a 12 | cross-platform 2D canvas. It's basic idea is giving you the simplest means 13 | possible to "just put pixels on the screen" without any of the fancy stuff. As a 14 | nice bonus you also get cross-platform keyboard/mouse input and frame timing in 15 | only a few lines of code. 16 | 17 | Read more about the idea behind fenster here: 18 | [Minimal cross-platform graphics - zserge.com](https://zserge.com/posts/fenster/) 19 | 20 | > [!NOTE] 21 | > This library is primarily intended for educational and prototyping purposes 22 | > and may not include all the features you would expect from a proper GUI 23 | > library. If you're looking for something more feature-rich and 24 | > production-ready, you might want to check out [LÖVE](https://love2d.org/) 25 | > or [raylib](https://www.raylib.com/). 26 | 27 | ## Installation 28 | 29 | From LuaRocks server: 30 | 31 | ```shell 32 | luarocks install fenster 33 | ``` 34 | 35 | From source: 36 | 37 | ```shell 38 | git clone https://github.com/jonasgeiler/lua-fenster.git 39 | cd lua-fenster 40 | luarocks make 41 | ``` 42 | 43 | ## Simple Example 44 | 45 | Here is a simple example that opens a 500x300 window, draws a red rectangle and 46 | exits when pressing the Escape key: 47 | 48 | ```lua 49 | -- rectangle.lua 50 | local fenster = require('fenster') 51 | 52 | local window = fenster.open(500, 300, 'Hello fenster!') 53 | 54 | while window:loop() and not window.keys[27] do 55 | window:clear() 56 | 57 | for y = 100, 200 do 58 | for x = 200, 300 do 59 | window:set(x, y, 0xff0000) 60 | end 61 | end 62 | end 63 | ``` 64 | 65 | To run the example: 66 | 67 | ```shell 68 | lua rectangle.lua 69 | ``` 70 | 71 | Here is what you should see: 72 | 73 | ![rectangle screenshot](https://github.com/jonasgeiler/lua-fenster/assets/10259118/7c9e495e-409b-4b3b-b232-c20da1ebfc88) 74 | 75 | ## Demos 76 | 77 | Check out the [./demos](./demos) folder for more elaborate example applications! 78 | To run a demo use: 79 | 80 | ```shell 81 | lua demos/.lua 82 | ``` 83 | 84 | Some of the demos are user-contributed. If you have a demo you'd like to share, 85 | feel free to [create a pull request](https://github.com/jonasgeiler/lua-fenster/new/main/demos)! 86 | 87 | ## Useful Snippets 88 | 89 | I have compiled a collection of useful snippets in 90 | [this discussion (#11)](https://github.com/jonasgeiler/lua-fenster/discussions/11). 91 | Check them out and maybe add your own! 92 | 93 | ## Type Definitions 94 | 95 | I have created type definitions for lua-fenster for the 96 | [Teal Programming Language](https://github.com/teal-language/tl) and 97 | [Sumneko's Lua Language Server](https://github.com/LuaLS/lua-language-server). 98 | You can find the Teal type definitions in the 99 | [teal-language/teal-types](https://github.com/teal-language/teal-types/tree/master/types/fenster) 100 | repository and the Lua Language Server type definitions in the 101 | [LuaLS/LLS-Addons](https://github.com/LuaLS/LLS-Addons/tree/main/addons/fenster) 102 | repository. 103 | Consult their respective documentation on how to use these type definitions in 104 | your projects. 105 | 106 | ## Projects using lua-fenster 107 | 108 | Here is a list of projects that use lua-fenster: 109 | 110 | - [3d-soft-engine-lua](https://github.com/jonasgeiler/3d-soft-engine-lua) - A simple 3D engine, by [Jonas Geiler (@jonasgeiler)](https://github.com/jonasgeiler). 111 | - [3d-raycaster-lua](https://github.com/jonasgeiler/3d-raycaster-lua) - A Wolfenstein-like 3D raycaster, by [Jonas Geiler (@jonasgeiler)](https://github.com/jonasgeiler). 112 | 113 | 124 | 125 | Feel free to add your own projects to this list by [creating a pull request](https://github.com/jonasgeiler/lua-fenster/edit/main/README.md)! 126 | 127 | ## API Documentation 128 | 129 | Here is a documentation of all functions, methods and properties provided by the 130 | fenster Lua module: 131 | 132 | - [`fenster.open(width: integer, height: integer, title: string | nil, scale: integer | nil, targetfps: number | nil): userdata`](#fensteropenwidth-integer-height-integer-title-string--nil-scale-integer--nil-targetfps-number--nil-userdata) 133 | 134 | - [`fenster.sleep(milliseconds: integer)`](#fenstersleepmilliseconds-integer) 135 | 136 | - [`fenster.time(): integer`](#fenstertime-integer) 137 | 138 | - [`fenster.rgb(redorcolor: integer, green: integer | nil, blue: integer | nil): integer, integer | nil, integer | nil`](#fensterrgbredorcolor-integer-green-integer--nil-blue-integer--nil-integer-integer--nil-integer--nil) 139 | 140 | - [`window:close()`](#windowclose) 141 | 142 | - [`window:loop()`](#windowloop-boolean) 143 | 144 | - [`window:set(x: integer, y: integer, color: integer)`](#windowsetx-integer-y-integer-color-integer) 145 | 146 | - [`window:get(x: integer, y: integer): integer`](#windowgetx-integer-y-integer-integer) 147 | 148 | - [`window:clear(color: integer | nil)`](#windowclearcolor-integer--nil) 149 | 150 | - [`window.keys: boolean[]`](#windowkeys-boolean) 151 | 152 | - [`window.delta: number`](#windowdelta-number) 153 | 154 | - [`window.mousex: integer`](#windowmousex-integer) 155 | 156 | - [`window.mousey: integer`](#windowmousey-integer) 157 | 158 | - [`window.mousedown: boolean`](#windowmousedown-boolean) 159 | 160 | - [`window.modcontrol: boolean`](#windowmodcontrol-boolean) 161 | 162 | - [`window.modshift: boolean`](#windowmodshift-boolean) 163 | 164 | - [`window.modalt: boolean`](#windowmodalt-boolean) 165 | 166 | - [`window.modgui: boolean`](#windowmodgui-boolean) 167 | 168 | - [`window.width: integer`](#windowwidth-integer) 169 | 170 | - [`window.height: integer`](#windowheight-integer) 171 | 172 | - [`window.title: string`](#windowtitle-string) 173 | 174 | - [`window.scale: integer`](#windowscale-integer) 175 | 176 | - [`window.targetfps: number`](#windowtargetfps-number) 177 | 178 | ### `fenster.open(width: integer, height: integer, title: string | nil, scale: integer | nil, targetfps: number | nil): userdata` 179 | 180 | This function is used to create a new window for your application. 181 | 182 | **Parameters:** 183 | 184 | - `width` (integer): The width of the window in pixels. 185 | 186 | - `height` (integer): The height of the window in pixels. 187 | 188 | - `title` (string, optional): The title of the window. If not provided, the 189 | default title 'fenster' will be used. 190 | 191 | - `scale` (integer, optional): The scale factor for the window. This should be a 192 | power of 2 (e.g., 1, 2, 4, 8). If not provided, the default scale factor of 1 193 | will be used. This means that each pixel in your application corresponds to 194 | one pixel on the screen. A scale factor of 2 would mean that each pixel in 195 | your application corresponds to a 2x2 square of pixels on the screen, and so 196 | on. The scaling happens completely internally, and you won't have to worry 197 | about it in your code. It is just a way to make your application more visible 198 | on high-resolution screens without sacrificing performance. 199 | 200 | - `targetfps` (number, optional): The target frames per second (FPS) for the 201 | window. If not provided, the default target FPS of 60 will be used. This is 202 | used to limit the CPU usage of your application and get more consistent frame 203 | durations by pausing for a short time after each frame to reach the target 204 | FPS. You can set it to 0 to disable FPS handling and let your application run 205 | as fast as possible, but generally, you should keep it at the default value of 206 | 60 FPS. 207 | 208 | **Returns:** 209 | 210 | An userdata object representing the created window. This object can be used to 211 | interact with the window, such as drawing pixels, handling input, and 212 | reading the window's properties. 213 | 214 | **Example:** 215 | 216 | ```lua 217 | local fenster = require('fenster') 218 | 219 | -- Create a new window with a width of 500 pixels, a height of 300 pixels, a title of 'My Application', a scale factor of 2, and a target FPS of 60. 220 | local window = fenster.open(500, 300, 'My Application', 2, 60) 221 | ``` 222 | 223 | ### `fenster.sleep(milliseconds: integer)` 224 | 225 | This utility function is used to pause the program execution for a specified 226 | amount of time. 227 | 228 | **Parameters:** 229 | 230 | - `milliseconds` (integer): The amount of time, in milliseconds, for which the 231 | program execution should be paused. 232 | 233 | **Example:** 234 | 235 | ```lua 236 | local fenster = require('fenster') 237 | 238 | -- Pause the program execution for 2 seconds (2000 milliseconds) 239 | fenster.sleep(2000) 240 | ``` 241 | 242 | ### `fenster.time(): integer` 243 | 244 | This utility function is used to get the current time in milliseconds since the 245 | Unix epoch (January 1, 1970). This is similar to `os.time()`, but with 246 | milliseconds instead of seconds. 247 | 248 | **Returns:** 249 | 250 | The current time in milliseconds since the Unix epoch as an integer. 251 | 252 | **Example:** 253 | 254 | ```lua 255 | local fenster = require('fenster') 256 | 257 | -- Get the current time in milliseconds 258 | local time = fenster.time() 259 | ``` 260 | 261 | ### `fenster.rgb(redorcolor: integer, green: integer | nil, blue: integer | nil): integer, integer | nil, integer | nil` 262 | 263 | This utility function is used to convert RGB values to a single color integer or 264 | vice versa. 265 | 266 | **Parameters:** 267 | 268 | - `redorcolor` (integer): If only one argument is given, it is assumed to be a 269 | color integer and the function returns the red, green and blue components as 270 | separate values. If three arguments are given, this represents the red 271 | component of the color. 272 | 273 | - `green` (integer, optional): The green component of the color. This is 274 | required if `redorcolor` is the red component. 275 | 276 | - `blue` (integer, optional): The blue component of the color. This is required 277 | if `redorcolor` is the red component. 278 | 279 | **Returns:** 280 | 281 | - If only `redorcolor` is provided, the function returns three integers 282 | representing the red, green, and blue components of the color. 283 | - If `redorcolor`, `green`, and `blue` are provided, the function returns a 284 | single integer representing the color. 285 | 286 | **Example:** 287 | 288 | ```lua 289 | local fenster = require('fenster') 290 | 291 | -- Convert RGB values to a single color integer 292 | local color = fenster.rgb(255, 0, 0) -- Returns: 0xff0000 (16711680 in decimal) 293 | 294 | -- Convert a single color integer to RGB values 295 | local red, green, blue = fenster.rgb(0xff0000) -- Returns: 255, 0, 0 296 | ``` 297 | 298 | ### `window:close()` 299 | 300 | This method is used to close a window that was previously opened 301 | with `fenster.open`. 302 | The `__gc` and `__close` (Lua 5.4) metamethods call this function internally to 303 | automatically close the window when it goes out of scope, so you won't 304 | have to call this function manually in most cases. 305 | 306 | **Example:** 307 | 308 | ```lua 309 | local fenster = require('fenster') 310 | 311 | -- Open a new window 312 | local window = fenster.open(500, 300, 'My Application', 2, 60) 313 | 314 | -- Close the window immediately 315 | window:close() 316 | ``` 317 | 318 | **Note:** 319 | 320 | In Lua 5.4 you can use 321 | [local variable attributes](https://www.lua.org/manual/5.4/manual.html#3.3.8) 322 | to immediately close the window when the `window` variable goes out of scope: 323 | 324 | ```lua 325 | local fenster = require('fenster') 326 | 327 | function main() 328 | -- Open a new window (note the attribute!) 329 | local window = fenster.open(500, 300, 'My Application', 2, 60) 330 | 331 | -- The window will be immediately closed when this function returns 332 | -- (Normally, garbage collection will also close the window but only at an 333 | -- undetermined time later on) 334 | end 335 | ``` 336 | 337 | ### `window:loop(): boolean` 338 | 339 | This method is used to handle the main loop for the window. It takes care of 340 | FPS limiting, updates delta time, keys, mouse coordinates, modifier keys, and 341 | the whole screen. 342 | 343 | **Returns:** 344 | 345 | A boolean value indicating whether the window is still open. It returns true if 346 | the window is still open and false if it's closed. 347 | 348 | > [!WARNING] 349 | > Currently it looks like only Windows returns false when the window is closed. 350 | > On Linux and macOS `fenster` just throws an error when closing the window... 351 | > 352 | > I am currently contemplating forking fenster in 353 | > [#24](https://github.com/jonasgeiler/lua-fenster/issues/24) 354 | > to fix this problem. 355 | 356 | **Example:** 357 | 358 | ```lua 359 | local fenster = require('fenster') 360 | 361 | -- Open a new window 362 | local window = fenster.open(500, 300, 'My Application', 2, 60) 363 | 364 | -- Handle the main loop for the window 365 | while window:loop() do 366 | -- ... Your code here... 367 | end 368 | ``` 369 | 370 | ### `window:set(x: integer, y: integer, color: integer)` 371 | 372 | This method is used to set a pixel in the window buffer at the given 373 | coordinates to the given color. 374 | 375 | **Parameters:** 376 | 377 | - `x` (integer): The x-coordinate of the pixel. 378 | 379 | - `y` (integer): The y-coordinate of the pixel. 380 | 381 | - `color` (integer): The color to set the pixel to. 382 | 383 | **Example:** 384 | 385 | ```lua 386 | local fenster = require('fenster') 387 | 388 | -- Open a new window 389 | local window = fenster.open(500, 300, 'My Application', 2, 60) 390 | 391 | -- Set the pixel at coordinates (10, 20) to red 392 | window:set(10, 20, 0xff0000) 393 | ``` 394 | 395 | ### `window:get(x: integer, y: integer): integer` 396 | 397 | This method is used to get the color of a pixel in the window buffer at the 398 | given coordinates. 399 | 400 | **Parameters:** 401 | 402 | - `x` (integer): The x-coordinate of the pixel. 403 | 404 | - `y` (integer): The y-coordinate of the pixel. 405 | 406 | **Returns:** 407 | 408 | An integer representing the color of the pixel at the given coordinates. 409 | 410 | **Example:** 411 | 412 | ```lua 413 | local fenster = require('fenster') 414 | 415 | -- Open a new window 416 | local window = fenster.open(500, 300, 'My Application', 2, 60) 417 | 418 | -- Set the pixel at coordinates (10, 20) to green 419 | window:set(10, 20, 0x00ff00) 420 | 421 | -- Get the color of the pixel at coordinates (10, 20) 422 | local color = window:get(10, 20) -- Returns: 0x00ff00 (65280 in decimal) 423 | ``` 424 | 425 | ### `window:clear(color: integer | nil)` 426 | 427 | This method is used to clear the window buffer with a given color. This can 428 | also be used to set a background color for the window. 429 | 430 | **Parameters:** 431 | 432 | - `color` (integer, optional): The color to fill the window buffer with. If not 433 | provided, the default color `0x000000` (black) is used. 434 | 435 | **Example:** 436 | 437 | ```lua 438 | local fenster = require('fenster') 439 | 440 | -- Open a new window 441 | local window = fenster.open(500, 300, 'My Application', 2, 60) 442 | 443 | -- Clear the window buffer with the color blue 444 | window:clear(0x0000ff) 445 | ``` 446 | 447 | ### `window.keys: boolean[]` 448 | 449 | This property is an array of boolean values representing the state of each key 450 | on the keyboard. Each index in the array corresponds to a specific key, and the 451 | value at that index is `true` if the key is currently pressed, and `false` 452 | otherwise. 453 | The key codes are mostly ASCII, but arrow keys are 17 to 20. 454 | 455 | **Example:** 456 | 457 | ```lua 458 | local fenster = require('fenster') 459 | 460 | -- Open a new window 461 | local window = fenster.open(500, 300, 'My Application', 2, 60) 462 | 463 | -- Get the ASCII code for the "F" key (70) 464 | local fkey = string.byte('F') 465 | 466 | -- Handle the main loop for the window 467 | while window:loop() do 468 | -- Check if the "F" key is pressed 469 | if window.keys[fkey] then 470 | -- Print a message 471 | print('F is pressed.') 472 | end 473 | end 474 | ``` 475 | 476 | ### `window.delta: number` 477 | 478 | This property contains the time in seconds that has passed since the last 479 | frame was rendered. This property is useful for creating smooth animations 480 | and movement, as you can use it to adjust the speed of an object based on the 481 | frame rate. 482 | 483 | Read more about delta time here: 484 | [Delta timing - Wikipedia](https://wikipedia.org/wiki/Delta_timing) 485 | 486 | **Example:** 487 | 488 | ```lua 489 | local fenster = require('fenster') 490 | 491 | -- Try out these values and notice the difference: 492 | local targetfps = 60 493 | --local targetfps = 30 494 | --local targetfps = 15 495 | 496 | -- Open a new window (very wide and scaled to see the pixels moving) 497 | local window = fenster.open(100, 40, 'My Application', 4, targetfps) 498 | 499 | -- Calculate the y position of the first pixel (center minus 10) 500 | local pixel1y = window.height / 2 - 10 501 | 502 | -- Initialize the x position of the first pixel 503 | local pixel1x = 0 504 | 505 | -- Calculate the x position of the second pixel (center plus 10) 506 | local pixel2y = window.height / 2 + 10 507 | 508 | -- Initialize the x position of the second pixel 509 | local pixel2x = 0 510 | 511 | -- Handle the main loop for the window 512 | while window:loop() do 513 | -- Clear the screen for redraw 514 | window:clear() 515 | 516 | -- Draw the first pixel in red (we have to floor the x position, because it has to be an integer) 517 | window:set(math.floor(pixel1x), pixel1y, 0xff0000) 518 | 519 | -- Move the first pixel to the right (no delta time) 520 | pixel1x = pixel1x + 0.35 521 | 522 | -- Reset the first pixel if it reaches the right edge of the window 523 | if pixel1x >= window.width then pixel1x = 0 end 524 | 525 | -- Draw the second pixel in green (also floor here) 526 | window:set(math.floor(pixel2x), pixel2y, 0x00ff00) 527 | 528 | -- Move the second pixel to the right (with delta time) 529 | pixel2x = pixel2x + 20 * window.delta 530 | 531 | -- Reset the second pixel if it reaches the right edge of the window 532 | if pixel2x >= window.width then pixel2x = 0 end 533 | end 534 | ``` 535 | 536 | ### `window.mousex: integer` 537 | 538 | This property contains the x-coordinate of the mouse cursor relative to the 539 | window. The coordinate system is the same as the window buffer, so you can pass 540 | it directly to the `window:set(...)` method to draw on the screen. 541 | 542 | **Example:** 543 | 544 | ```lua 545 | local fenster = require('fenster') 546 | 547 | -- Open a new window 548 | local window = fenster.open(500, 300, 'My Application', 2, 60) 549 | 550 | -- Handle the main loop for the window 551 | while window:loop() do 552 | -- Draw a cyan pixel at the mouse position 553 | window:set(window.mousex, window.mousey, 0x00ffff) 554 | end 555 | ``` 556 | 557 | ### `window.mousey: integer` 558 | 559 | This property contains the y-coordinate of the mouse cursor relative to the 560 | window. See [`window.mousex`](#windowmousex-integer) for more information. 561 | 562 | ### `window.mousedown: boolean` 563 | 564 | This property contains the state of the mouse button. If the mouse button is 565 | currently pressed, the value will be `true`, otherwise it will be `false`. 566 | Currently, all mouse buttons are treated as the same button, so you can't 567 | distinguish between left, right, or middle mouse buttons. 568 | 569 | **Example:** 570 | 571 | ```lua 572 | local fenster = require('fenster') 573 | 574 | -- Open a new window 575 | local window = fenster.open(500, 300, 'My Application', 2, 60) 576 | 577 | -- Handle the main loop for the window 578 | while window:loop() do 579 | -- Check if the mouse is pressed 580 | if window.mousedown then 581 | -- Draw a yellow pixel at the mouse position 582 | window:set(window.mousex, window.mousey, 0xffff00) 583 | end 584 | end 585 | ``` 586 | 587 | ### `window.modcontrol: boolean` 588 | 589 | This property contains the state of the Control key, also known as the Ctrl key. 590 | If the Control key is currently pressed, the value will be `true`, otherwise it 591 | will be `false`. 592 | 593 | > [!WARNING] 594 | > In my experience, the states of the modifier keys are only updated when 595 | > another key is pressed simultaneously, so you might not get the expected 596 | > behavior if you only check the modifier property. 597 | > 598 | > I am currently contemplating forking fenster in 599 | > [#24](https://github.com/jonasgeiler/lua-fenster/issues/24) 600 | > to fix this problem. 601 | 602 | **Example:** 603 | 604 | ```lua 605 | local fenster = require('fenster') 606 | 607 | -- Open a new window 608 | local window = fenster.open(500, 300, 'My Application', 2, 60) 609 | 610 | -- Get the ASCII code for the "G" key (71) 611 | local gkey = string.byte('G') 612 | 613 | -- Handle the main loop for the window 614 | while window:loop() do 615 | -- Check if the "G" key is pressed 616 | if window.keys[gkey] then 617 | local keycombination = {} 618 | 619 | -- Check which modifier key is pressed and add it to the key combination 620 | if window.modcontrol then 621 | keycombination[#keycombination + 1] = 'Ctrl' 622 | end 623 | if window.modshift then 624 | keycombination[#keycombination + 1] = 'Shift' 625 | end 626 | if window.modalt then 627 | keycombination[#keycombination + 1] = 'Alt' 628 | end 629 | if window.modgui then 630 | keycombination[#keycombination + 1] = 'GUI' 631 | end 632 | 633 | -- Add the "G" key to the key combination 634 | keycombination[#keycombination + 1] = 'G' 635 | 636 | -- Print the key combination that is pressed 637 | print(table.concat(keycombination, ' + ') .. ' is pressed.') 638 | end 639 | end 640 | ``` 641 | 642 | ### `window.modshift: boolean` 643 | 644 | This property contains the state of the Shift key. See 645 | [`window.modcontrol`](#windowmodcontrol-boolean) for more information. 646 | 647 | ### `window.modalt: boolean` 648 | 649 | This property contains the state of the Alt key. See 650 | [`window.modcontrol`](#windowmodcontrol-boolean) for more information. 651 | 652 | ### `window.modgui: boolean` 653 | 654 | This property contains the state of the GUI key, also known as the Windows, 655 | Command or Meta key. See [`window.modcontrol`](#windowmodcontrol-boolean) for 656 | more information. 657 | 658 | ### `window.width: integer` 659 | 660 | This property contains the width of the window. Note that the width of the 661 | window is read-only and cannot be updated, like all other properties of the 662 | window object. 663 | 664 | **Example:** 665 | 666 | ```lua 667 | local fenster = require('fenster') 668 | 669 | -- Open a new window 670 | local window = fenster.open(500, 300, 'My Application', 2, 60) 671 | 672 | -- Print the width of the window 673 | print(window.width) -- Output: 500 674 | ``` 675 | 676 | ### `window.height: integer` 677 | 678 | This property contains the height of the window. Note that the height of the 679 | window is read-only and cannot be updated, like all other properties of the 680 | window object. 681 | 682 | **Example:** 683 | 684 | ```lua 685 | local fenster = require('fenster') 686 | 687 | -- Open a new window 688 | local window = fenster.open(500, 300, 'My Application', 2, 60) 689 | 690 | -- Print the height of the window 691 | print(window.height) -- Output: 300 692 | ``` 693 | 694 | ### `window.title: string` 695 | 696 | This property contains the title of the window. Note that the title of the 697 | window is read-only and cannot be updated, like all other properties of the 698 | window object. 699 | 700 | **Example:** 701 | 702 | ```lua 703 | local fenster = require('fenster') 704 | 705 | -- Open a new window 706 | local window = fenster.open(500, 300, 'My Application', 2, 60) 707 | 708 | -- Print the title of the window 709 | print(window.title) -- Output: My Application 710 | ``` 711 | 712 | ### `window.scale: integer` 713 | 714 | This property contains the scale factor of the window. Note that the scale of 715 | the window is read-only and cannot be updated, like all other properties of the 716 | window object. If you are using this property for reasons other than debugging, 717 | you are probably doing something wrong, as the scaling happens completely 718 | internally and you won't have to worry about it in your code. 719 | 720 | **Example:** 721 | 722 | ```lua 723 | local fenster = require('fenster') 724 | 725 | -- Open a new window 726 | local window = fenster.open(500, 300, 'My Application', 2, 60) 727 | 728 | -- Print the scale of the window 729 | print(window.scale) -- Output: 2 730 | ``` 731 | 732 | ### `window.targetfps: number` 733 | 734 | This property contains the target frames per second (FPS) of the window. Note 735 | that the target FPS of the window is read-only and cannot be updated, like all 736 | other properties of the window object. 737 | 738 | **Example:** 739 | 740 | ```lua 741 | local fenster = require('fenster') 742 | 743 | -- Open a new window 744 | local window = fenster.open(500, 300, 'My Application', 2, 60) 745 | 746 | -- Print the target FPS of the window 747 | print(window.targetfps) -- Output: 60.0 748 | ``` 749 | 750 | ## Development 751 | 752 | I am developing on Linux, so I will only be able to provide a guide for Linux. 753 | 754 | ### Building 755 | 756 | Building the library from the source code requires: 757 | 758 | - GCC or similar (f.e. `apt install build-essential`) 759 | - X11 Development Files (f.e. `apt install libx11-dev`) 760 | - Lua (f.e. `apt install lua5.4`) 761 | - Lua Development Files (f.e. `apt install liblua5.4-dev`) 762 | - LuaRocks (f.e. `apt install luarocks`) 763 | 764 | To build the library from the source code, use: 765 | 766 | ```shell 767 | luarocks make 768 | ``` 769 | 770 | > [!TIP] 771 | > If you have multiple Lua versions installed on your system, you can specify 772 | > the version to build for with the `--lua-version` LuaRocks flag. For 773 | > example, to build for Lua 5.4, use: 774 | > 775 | > ```shell 776 | > luarocks --lua-version=5.4 make 777 | > ``` 778 | 779 | ### Testing 780 | 781 | Before you can run the test you should [build the library](#building) first. 782 | 783 | Afterward, to run the tests, use: 784 | 785 | ```shell 786 | luarocks test 787 | ``` 788 | 789 | > [!TIP] 790 | > If you have multiple Lua versions installed on your system, you can specify 791 | > the version to use for testing with the `--lua-version` LuaRocks flag. For 792 | > example, to test with Lua 5.4, use: 793 | > 794 | > ```shell 795 | > luarocks --lua-version=5.4 test 796 | > ``` 797 | 798 | ### Testing using Docker 799 | 800 | If you don't want to install the dependencies above on your system, or want to 801 | test on all Lua versions simultaneously in a clean environment, you can use 802 | Docker with Docker Compose. 803 | 804 | Just run the following command to build the Docker images for all Lua 805 | versions and run the tests on each: 806 | 807 | ```shell 808 | docker compose up --build --force-recreate --abort-on-container-failure 809 | ``` 810 | 811 | ## Credits 812 | 813 | Many thanks to [Serge Zaitsev (@zserge)](https://github.com/zserge) for creating 814 | the original [fenster](https://github.com/zserge/fenster) library and making it 815 | available to the public. This Lua binding wouldn't have been possible without 816 | their work. 817 | 818 | ## License 819 | 820 | This project is licensed under the [MIT License](./LICENSE.md). Feel free to use 821 | it in your own proprietary or open-source projects. If you have any questions, 822 | please open an issue or discussion! 823 | -------------------------------------------------------------------------------- /demos/assets/uv.ppm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasgeiler/lua-fenster/ece948215f83068671d2af6a062abc35bc734d6e/demos/assets/uv.ppm -------------------------------------------------------------------------------- /demos/fractal.lua: -------------------------------------------------------------------------------- 1 | local fenster = require('fenster') 2 | 3 | ---Map a value from one range to another 4 | ---@param value number 5 | ---@param start1 number 6 | ---@param stop1 number 7 | ---@param start2 number 8 | ---@param stop2 number 9 | ---@return number 10 | ---@nodiscard 11 | local function map(value, start1, stop1, start2, stop2) 12 | return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1)) 13 | end 14 | 15 | -- Open a window 16 | local window_width = 144 17 | local window_height = 144 18 | local window_scale = 4 19 | local window = fenster.open( 20 | window_width, 21 | window_height, 22 | 'Fractal Demo - Press ESC to exit', 23 | window_scale 24 | ) 25 | 26 | -- Fractal settings 27 | local fractal_depth = 64 28 | local generation_infinity = 16 29 | 30 | -- Get range of the fractal 31 | local range = 2 32 | local x_min = 0 - range 33 | local x_max = 0 + range 34 | local y_min = 0 - range 35 | local y_max = 0 + range 36 | 37 | -- Display the fractal 38 | local angle = 0 39 | while window:loop() and not window.keys[27] do 40 | -- Draw the fractal 41 | for y = 0, window_height - 1 do 42 | for x = 0, window_width - 1 do 43 | local real = map(x, 0, window_width, x_min, x_max) 44 | local imag = map(y, 0, window_height, y_min, y_max) 45 | 46 | local depth = 0 47 | while depth < fractal_depth do 48 | local re = real * real - imag * imag 49 | local im = 2 * real * imag 50 | 51 | real = re + math.cos(angle) 52 | imag = im + math.sin(angle) 53 | 54 | if math.abs(real + imag) > generation_infinity then 55 | break 56 | end 57 | depth = depth + 1 58 | end 59 | 60 | local color = 0x000000 61 | if depth < fractal_depth then 62 | color = depth * 32 % 256 63 | end 64 | window:set(x, y, color) 65 | end 66 | end 67 | 68 | -- Rotate the fractal 69 | angle = angle + 2 * window.delta 70 | end 71 | -------------------------------------------------------------------------------- /demos/game-of-life.lua: -------------------------------------------------------------------------------- 1 | -- Game of Life Demo by Paul Adam 2 | -- https://github.com/pauladam94 3 | 4 | local fenster = require('fenster') 5 | 6 | local window_width = 200 7 | local window_height = 200 8 | local window_scale = 4 9 | local window = fenster.open( 10 | window_width, 11 | window_height, 12 | 'Game of Life Demo - Press ESC to exit', 13 | window_scale 14 | ) 15 | 16 | local neighbours = { { -1, -1 }, { -1, 0 }, { 0, -1 }, { -1, 1 }, { 1, -1 }, { 1, 0 }, { 0, 1 }, { 1, 1 } } 17 | 18 | ---@param x integer 19 | ---@param y integer 20 | ---@param world boolean[][] 21 | ---@return integer 22 | local function count_alive_neighbours(x, y, world) 23 | local count = 0 24 | for _, neighbour in pairs(neighbours) do 25 | local dx = x + neighbour[1] 26 | local dy = y + neighbour[2] 27 | if dx >= 1 and dx <= window_width and dy <= window_height and dy >= 1 then 28 | if world[dx][dy] then 29 | count = count + 1 30 | end 31 | end 32 | end 33 | return count 34 | end 35 | 36 | ---@param obj any 37 | ---@return any 38 | local function copy(obj) 39 | if type(obj) ~= 'table' then return obj end 40 | local res = {} ---@type table 41 | for k, v in pairs(obj) do res[copy(k)] = copy(v) end 42 | return res 43 | end 44 | 45 | local world = {} ---@type boolean[][] 46 | for x = 1, window_width do 47 | world[x] = {} 48 | for y = 1, window_height do 49 | world[x][y] = false 50 | if x % 2 == 0 then 51 | world[x][y] = true 52 | end 53 | end 54 | end 55 | 56 | while window:loop() and not window.keys[27] do 57 | local previous_world = copy(world) 58 | for x = 1, window_width do 59 | for y = 1, window_height do 60 | if previous_world[x][y] then 61 | window:set(x - 1, y - 1, 0xffffff) 62 | else 63 | window:set(x - 1, y - 1, 0x000000) 64 | end 65 | local count = count_alive_neighbours(x, y, previous_world) 66 | world[x][y] = (count == 3) or (previous_world[x][y] and count == 2) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /demos/heart.lua: -------------------------------------------------------------------------------- 1 | -- Heart Demo by turgenevivan 2 | -- https://github.com/turgenevivan 3 | 4 | local fenster = require('fenster') 5 | 6 | local width = 144 7 | local height = 144 8 | local window_scale = 4 9 | local fps = 30 10 | 11 | local window = fenster.open(width, height, 'Heart Demo - Press ESC to exit, 1-4 to change type', window_scale, fps) 12 | 13 | -- Heart shape function 14 | local function heart(x, y) 15 | -- https://mathworld.wolfram.com/HeartCurve.html 16 | -- Convert window coordinates to mathematical coordinates, using window center as origin 17 | local xp = (x - width / 2) / (width / 3) -- Scale factor for x 18 | local yp = (height / 2 - y) / (height / 3) -- Scale and invert y because y increases downwards 19 | 20 | -- Heart equation: (x^2 + y^2 - 1)^3 = x^2 * y^3 21 | return (xp ^ 2 + yp ^ 2 - 1) ^ 3 - xp ^ 2 * yp ^ 3 <= 0 22 | end 23 | 24 | -- Define the heart shape function with a pattern using level sets 25 | local function heart_with_pattern(x, y) 26 | -- https://en.wikipedia.org/wiki/Level_set 27 | -- Convert window coordinates to mathematical coordinates, using window center as origin 28 | local xp = (x - width / 2) / (width / 3) -- Scale factor for x 29 | local yp = (height / 2 - y) / (height / 3) -- Scale and invert y 30 | 31 | -- Heart level set function 32 | local z = xp * xp + yp * yp - 1 33 | local f = z * z * z - xp * xp * yp * yp * yp 34 | 35 | -- Mapping result to color intensity (like characters in the original code) 36 | if f <= 0.0 then 37 | -- Map 'f' value to grayscale intensity based on the same idea as character mapping 38 | local intensity = math.floor((f * -8.0) % 8) -- Same concept as indexing into characters 39 | local shades = { 40 | 0x330000, -- Dark red (represents '@') 41 | 0x660000, -- Darker red (represents '%') 42 | 0x990000, -- Medium dark red (represents '#') 43 | 0xcc0000, -- Medium red (represents '*') 44 | 0xff0000, -- Bright red (represents '+') 45 | 0xff3333, -- Light red (represents '=') 46 | 0xff6666, -- Lighter red (represents ':') 47 | 0xffffff, -- White (represents '.') 48 | } 49 | return shades[intensity + 1] -- Return color based on intensity 50 | else 51 | return 0xffffff -- Background white 52 | end 53 | end 54 | 55 | -- Define the heart shape function f(x, y, z) 56 | local function f(x, y, z) 57 | local a = x * x + (9.0 / 4.0) * y * y + z * z - 1 58 | return a * a * a - x * x * z * z * z - (9.0 / 80.0) * y * y * z * z * z 59 | end 60 | 61 | -- Define h(x, z) function to find the surface at a given x and z 62 | local function h(x, z) 63 | for y = 1.0, 0.0, -0.001 do 64 | if f(x, y, z) <= 0.0 then 65 | return y 66 | end 67 | end 68 | return 0.0 69 | end 70 | 71 | 72 | local function draw_heart_with_pattern() 73 | for x = 0, width - 1 do 74 | for y = 0, height - 1 do 75 | local color = heart_with_pattern(x, y) 76 | window:set(x, y, color) -- Set pixel color based on the level set function using hex 77 | end 78 | end 79 | end 80 | 81 | local function draw_heart() 82 | for x = 0, width - 1 do 83 | for y = 0, height - 1 do 84 | if heart(x, y) then 85 | window:set(x, y, 0xff0000) -- Set red color for the heart shape 86 | end 87 | end 88 | end 89 | end 90 | 91 | local function draw_heart3d() 92 | for sy = 0, height - 1 do -- same 93 | local z = 1.5 - sy * 3.0 / height -- Map screen coordinates to z 94 | for sx = 0, width - 1 do 95 | local x = sx * 3.0 / width - 1.5 -- Map screen coordinates to x 96 | local v = f(x, 0.0, z) -- Evaluate the heart equation 97 | local r = 0 -- Red channel value 98 | 99 | if v <= 0.0 then 100 | local y0 = h(x, z) -- Get the y value at (x, z) 101 | local ny = 0.0011 -- Small offset for normal calculation 102 | local nx = h(x + ny, z) - y0 -- Calculate the normal x component 103 | local nz = h(x, z + ny) - y0 -- Calculate the normal z component 104 | local nd = 1.0 / math.sqrt(nx * nx + ny * ny + nz * nz) -- Normalize 105 | local d = (nx + ny - nz) * nd * 0.5 + 0.5 -- Calculate brightness 106 | r = math.floor(d * 255.0) -- Map to 0-255 range 107 | end 108 | 109 | -- Ensure red channel is clamped between 0 and 255 110 | r = math.max(0, math.min(255, r)) 111 | 112 | -- Set the pixel color (R, G, B) 113 | window:set(sx, sy, (r * 2 ^ 16) + (0 * 2 ^ 8) + 0) -- Only red color 114 | end 115 | end 116 | end 117 | 118 | -- Draw the heart shape with color 119 | local function draw_heart3d_animate(t) 120 | -- Heart scaling based on time (to create the pulsing effect) 121 | local s = math.sin(t) 122 | local a = s * s * s * s * 0.2 -- Scaling factor based on sine wave 123 | 124 | for sy = 0, height - 1 do 125 | local z = 1.5 - sy * 3.0 / height -- Map screen coordinates to z 126 | z = z * (1.2 - a) -- animate 127 | for sx = 0, width - 1 do 128 | local x = sx * 3.0 / width - 1.5 -- Map screen coordinates to x 129 | x = x * (1.2 + a) -- animate 130 | local v = f(x, 0.0, z) -- Evaluate the heart equation 131 | local r = 0 -- Red channel value 132 | 133 | if v <= 0.0 then 134 | local y0 = h(x, z) -- Get the y value at (x, z) 135 | local ny = 0.0011 -- Small offset for normal calculation 136 | local nx = h(x + ny, z) - y0 -- Calculate the normal x component 137 | local nz = h(x, z + ny) - y0 -- Calculate the normal z component 138 | local nd = 1.0 / math.sqrt(nx * nx + ny * ny + nz * nz) -- Normalize 139 | local d = (nx + ny - nz) * nd * 0.5 + 0.5 -- Calculate brightness 140 | r = math.floor(d * 255.0) -- Map to 0-255 range 141 | end 142 | 143 | -- Ensure red channel is clamped between 0 and 255 144 | r = math.max(0, math.min(255, r)) 145 | 146 | -- Set the pixel color (R, G, B) 147 | local color = fenster.rgb(r, 0, 0) 148 | window:set(sx, sy, color) -- Only red color 149 | end 150 | end 151 | end 152 | 153 | 154 | local draw_menu = { 155 | [1] = draw_heart, 156 | [2] = draw_heart_with_pattern, 157 | [3] = draw_heart3d, 158 | [4] = draw_heart3d_animate, 159 | } 160 | local draw_type = 1 161 | 162 | local t = 0 163 | while window:loop() and not window.keys[27] do 164 | window:clear() 165 | 166 | if window.keys[string.byte('1')] then 167 | draw_type = 1 168 | elseif window.keys[string.byte('2')] then 169 | draw_type = 2 170 | elseif window.keys[string.byte('3')] then 171 | draw_type = 3 172 | elseif window.keys[string.byte('4')] then 173 | t = 0 174 | draw_type = 4 175 | end 176 | 177 | draw_menu[draw_type](t) 178 | t = t + 3 * window.delta -- Update time 179 | end 180 | -------------------------------------------------------------------------------- /demos/image.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | local fenster = require('fenster') 4 | 5 | -- Hack to get the current script directory 6 | local dirname = './' .. (debug.getinfo(1, 'S').source:match('^@?(.*[/\\])') or '') ---@type string 7 | -- Add the project root directory to the package path 8 | package.path = dirname .. '../?.lua;' .. package.path 9 | 10 | local ppm = require('demos.lib.ppm') 11 | 12 | ---Draw a loaded image. 13 | ---@param window window* 14 | ---@param x integer 15 | ---@param y integer 16 | ---@param image_pixels integer[][] 17 | ---@param image_width integer 18 | ---@param image_height integer 19 | local function draw_image(window, x, y, image_pixels, image_width, image_height) 20 | for iy = 1, image_height do 21 | local dy = y + (iy - 1) 22 | for ix = 1, image_width do 23 | window:set(x + (ix - 1), dy, image_pixels[iy][ix]) 24 | end 25 | end 26 | end 27 | 28 | -- Load either a user-specified image or the default image 29 | local image_path = arg[1] or dirname .. 'assets/uv.ppm' 30 | local image_pixels, image_width, image_height, image_err = ppm.load(image_path) 31 | if not image_pixels or not image_width or not image_height then 32 | print('Failed to load image: ' .. tostring(image_err)) 33 | return 34 | end 35 | 36 | -- Calculate the window scale 37 | local window_scale = 1 38 | while image_width * window_scale < 512 and image_height * window_scale < 512 do 39 | window_scale = window_scale * 2 40 | end 41 | 42 | -- Open a window 43 | local window = fenster.open( 44 | image_width, 45 | image_height, 46 | 'Image Demo - Press ESC to exit', 47 | window_scale 48 | ) 49 | 50 | -- Draw the image 51 | draw_image( 52 | window, 53 | 0, 54 | 0, 55 | image_pixels, 56 | image_width, 57 | image_height 58 | ) 59 | 60 | -- Empty window loop 61 | while window:loop() and not window.keys[27] do 62 | -- 63 | end 64 | -------------------------------------------------------------------------------- /demos/lib/ppm.lua: -------------------------------------------------------------------------------- 1 | local io = io 2 | local string = string 3 | local math = math 4 | local fenster = require('fenster') 5 | 6 | local ppm = {} 7 | ppm.INT_MAX = 0x7fffffff 8 | ppm.CHAR_0 = 48 -- '0' in ASCII 9 | ppm.CHAR_9 = 57 -- '9' in ASCII 10 | 11 | ---Load a PPM file. (Adapted from Netbpm library v10.86.45) 12 | ---@param path string 13 | ---@return integer[][]? pixels 14 | ---@return integer? width 15 | ---@return integer? height 16 | ---@return string? errmsg 17 | ---@nodiscard 18 | function ppm.load(path) 19 | local ppm_file, ppm_file_err = io.open(path, 'rb') 20 | if not ppm_file then 21 | return nil, nil, nil, 'Failed to open file: ' .. ppm_file_err 22 | end 23 | 24 | -- Read magic number 25 | local char1 = ppm_file:read(1) ---@type string? 26 | if not char1 then 27 | return nil, nil, nil, 'File is empty or invalid: missing magic number' 28 | end 29 | local char2 = ppm_file:read(1) ---@type string? 30 | if not char2 then 31 | return nil, nil, nil, string.format( 32 | 'Incomplete magic number: first byte read as 0x%02x ("%s"), but second byte is missing', 33 | string.byte(char1), 34 | char1 35 | ) 36 | end 37 | local format = char1 .. char2 38 | if format ~= 'P3' and format ~= 'P6' then 39 | return nil, nil, nil, string.format( 40 | 'Invalid magic number 0x%04x ("%s"): not a PPM file, expected 0x5033 ("P3") or 0x5036 ("P6")', 41 | string.byte(char1) * 256 + string.byte(char2), 42 | format 43 | ) 44 | end 45 | 46 | ---@return string? char 47 | ---@return string? errmsg 48 | ---@nodiscard 49 | local function read_char() 50 | local char = ppm_file:read(1) ---@type string? 51 | if not char then 52 | return nil, 'Unexpected end of file while reading a byte' 53 | end 54 | if char == '#' then 55 | repeat 56 | char = ppm_file:read(1) ---@type string? 57 | if not char then 58 | return nil, 'Unexpected end of file while reading a comment' 59 | end 60 | until char == '\n' or char == '\r' 61 | end 62 | return char, nil 63 | end 64 | 65 | ---@return integer? uint 66 | ---@return string? errmsg 67 | ---@nodiscard 68 | local function read_uint() 69 | local char, char_err ---@type string?, string? 70 | repeat 71 | char, char_err = read_char() 72 | if not char then return nil, char_err end 73 | until char ~= ' ' and char ~= '\t' and char ~= '\n' and char ~= '\r' 74 | local char_byte = string.byte(char) 75 | if char_byte < ppm.CHAR_0 or char_byte > ppm.CHAR_9 then 76 | return nil, 'Expected an unsigned integer but found invalid data' 77 | end 78 | 79 | local uint = 0 80 | repeat 81 | local digit_val = char_byte - ppm.CHAR_0 82 | if uint > ppm.INT_MAX / 10 then 83 | return nil, 'Integer value is too large to process' 84 | end 85 | uint = uint * 10 86 | if uint > ppm.INT_MAX - digit_val then 87 | return nil, 'Integer value is too large to process' 88 | end 89 | uint = uint + digit_val ---@type integer 90 | 91 | char, char_err = read_char() 92 | if not char then return nil, char_err end 93 | char_byte = string.byte(char) 94 | until char_byte < ppm.CHAR_0 or char_byte > ppm.CHAR_9 95 | 96 | return uint, nil 97 | end 98 | 99 | -- Read width 100 | local cols, cols_err = read_uint() 101 | if not cols then 102 | return nil, nil, nil, 'Failed to read image width: ' .. cols_err 103 | end 104 | if cols > ppm.INT_MAX - 2 then 105 | return nil, nil, nil, string.format('Image width (%u) exceeds the maximum supported size', cols) 106 | end 107 | 108 | -- Read height 109 | local rows, rows_err = read_uint() 110 | if not rows then 111 | return nil, nil, nil, 'Failed to read image height: ' .. rows_err 112 | end 113 | if rows > ppm.INT_MAX - 2 then 114 | return nil, nil, nil, string.format('Image height (%u) exceeds the maximum supported size', rows) 115 | end 116 | 117 | -- Read max value 118 | local maxval, maxval_err = read_uint() 119 | if not maxval then 120 | return nil, nil, nil, 'Failed to read max color value: ' .. maxval_err 121 | end 122 | if maxval > 65535 then 123 | return nil, nil, nil, string.format('Max color value (%u) exceeds the PPM format limit of 65535', maxval) 124 | end 125 | if maxval == 0 then 126 | return nil, nil, nil, 'Max color value cannot be zero' 127 | end 128 | 129 | ---Scale RGB values from 0 to maxval, to 0 to 255, and then turn them into a single integer. 130 | ---@param r integer 131 | ---@param g integer 132 | ---@param b integer 133 | ---@return integer 134 | ---@nodiscard 135 | local function scaled_rgb(r, g, b) 136 | if maxval == 255 then 137 | return fenster.rgb(r, g, b) 138 | end 139 | 140 | return fenster.rgb( 141 | math.floor(r * 255 / maxval), 142 | math.floor(g * 255 / maxval), 143 | math.floor(b * 255 / maxval) 144 | ) 145 | end 146 | 147 | ---@param pixelrow integer[] 148 | ---@return string? errmsg 149 | ---@nodiscard 150 | local function read_ppm_row(pixelrow) 151 | for col = 1, cols do 152 | local r, r_err = read_uint() 153 | if not r then 154 | return 'Failed to read red value: ' .. r_err 155 | end 156 | if r > maxval then 157 | return string.format('Red value (%u) exceeds max color value (%u)', r, maxval) 158 | end 159 | 160 | local g, g_err = read_uint() 161 | if not g then 162 | return 'Failed to read green value: ' .. g_err 163 | end 164 | if g > maxval then 165 | return string.format('Green value (%u) exceeds max color value (%u)', g, maxval) 166 | end 167 | 168 | local b, b_err = read_uint() 169 | if not b then 170 | return 'Failed to read blue value: ' .. b_err 171 | end 172 | if b > maxval then 173 | return string.format('Blue value (%u) exceeds max color value (%u)', b, maxval) 174 | end 175 | 176 | pixelrow[col] = scaled_rgb(r, g, b) 177 | end 178 | 179 | return nil 180 | end 181 | 182 | ---@param pixelrow integer[] 183 | ---@return string? errmsg 184 | ---@nodiscard 185 | local function read_raw_ppm_row(pixelrow) 186 | local bytes_per_sample = maxval < 256 and 1 or 2 187 | local bytes_per_row = cols * 3 * bytes_per_sample 188 | local row_buffer = ppm_file:read(bytes_per_row) ---@type string? 189 | if not row_buffer then 190 | return 'Unexpected end of file while reading a row of pixel data' 191 | end 192 | if #row_buffer ~= bytes_per_row then 193 | return string.format('Incomplete row: expected %u bytes but got %u', bytes_per_row, #row_buffer) 194 | end 195 | 196 | -- Read the row buffer 197 | local buffer_cursor = 1 -- Starts at beginning of row_buffer 198 | for col = 1, cols do 199 | local r, g, b ---@type integer, integer, integer 200 | if bytes_per_sample == 1 then 201 | r = string.byte(row_buffer, buffer_cursor) 202 | buffer_cursor = buffer_cursor + 1 ---@type integer 203 | g = string.byte(row_buffer, buffer_cursor) 204 | buffer_cursor = buffer_cursor + 1 205 | b = string.byte(row_buffer, buffer_cursor) 206 | buffer_cursor = buffer_cursor + 1 207 | else 208 | -- Two byte samples 209 | r = string.byte(row_buffer, buffer_cursor) * 256 + string.byte(row_buffer, buffer_cursor + 1) 210 | buffer_cursor = buffer_cursor + 2 ---@type integer 211 | g = string.byte(row_buffer, buffer_cursor) * 256 + string.byte(row_buffer, buffer_cursor + 1) 212 | buffer_cursor = buffer_cursor + 2 213 | b = string.byte(row_buffer, buffer_cursor) * 256 + string.byte(row_buffer, buffer_cursor + 1) 214 | buffer_cursor = buffer_cursor + 2 215 | end 216 | 217 | -- Validate colors 218 | if r > maxval then 219 | return string.format('Red value (%u) exceeds max color value (%u)', r, maxval) 220 | end 221 | if g > maxval then 222 | return string.format('Green value (%u) exceeds max color value (%u)', g, maxval) 223 | end 224 | if b > maxval then 225 | return string.format('Blue value (%u) exceeds max color value (%u)', b, maxval) 226 | end 227 | 228 | pixelrow[col] = scaled_rgb(r, g, b) 229 | end 230 | 231 | return nil 232 | end 233 | 234 | local pixels = {} ---@type integer[][] 235 | for row = 1, rows do 236 | pixels[row] = {} ---@type integer[] 237 | if format == 'P3' then 238 | local row_err = read_ppm_row(pixels[row]) 239 | if row_err then 240 | return nil, nil, nil, 'Error reading pixel data: ' .. row_err 241 | end 242 | else --if format == 'P6' then 243 | local row_err = read_raw_ppm_row(pixels[row]) 244 | if row_err then 245 | return nil, nil, nil, 'Error reading raw pixel data: ' .. row_err 246 | end 247 | end 248 | end 249 | 250 | return pixels, cols, rows, nil 251 | end 252 | 253 | return ppm 254 | -------------------------------------------------------------------------------- /demos/moving-box.lua: -------------------------------------------------------------------------------- 1 | local fenster = require('fenster') 2 | 3 | ---Draw a filled rectangle 4 | ---@param window window* 5 | ---@param x integer 6 | ---@param y integer 7 | ---@param width integer 8 | ---@param height integer 9 | ---@param color integer 10 | local function draw_rectangle(window, x, y, width, height, color) 11 | local dx_end = x + width - 1 12 | for dy = y, y + height - 1 do 13 | for dx = x, dx_end do 14 | window:set(dx, dy, color) 15 | end 16 | end 17 | end 18 | 19 | -- Rectangle settings 20 | local rect_width = 30 21 | local rect_height = 20 22 | local rect_speed = 50 23 | local rect_colors = { 24 | 0xff0000, 25 | 0x00ff00, 26 | 0xffff00, 27 | 0x0000ff, 28 | 0xff00ff, 29 | 0x00ffff, 30 | } 31 | 32 | -- Open a window 33 | local window_width = 256 34 | local window_height = 144 35 | local window_scale = 2 36 | local window = fenster.open( 37 | window_width, 38 | window_height, 39 | 'Moving Demo - Press ESC to exit', 40 | window_scale 41 | ) 42 | 43 | -- Draw a moving rectangle 44 | local rect_x, rect_y = 0, 0 45 | local rect_dir_x, rect_dir_y = 1, 1 46 | local rect_color_index = 1 47 | while window:loop() and not window.keys[27] do 48 | local delta = window.delta 49 | 50 | -- Clear the screen for redraw 51 | window:clear() 52 | 53 | -- Move the rectangle 54 | rect_x = rect_x + rect_speed * rect_dir_x * delta 55 | rect_y = rect_y + rect_speed * rect_dir_y * delta 56 | 57 | -- Check if the rectangle would be out of bounds 58 | if rect_x < 0 or rect_x + rect_width >= window_width then 59 | -- Flip the x direction 60 | rect_dir_x = -rect_dir_x 61 | 62 | -- Change the color 63 | rect_color_index = (rect_color_index % #rect_colors) + 1 64 | 65 | -- Recalculate the x position with the new direction 66 | rect_x = rect_x + rect_speed * rect_dir_x * delta ---@type number 67 | end 68 | if rect_y < 0 or rect_y + rect_height >= window_height then 69 | -- Flip the y direction 70 | rect_dir_y = -rect_dir_y 71 | 72 | -- Change the color 73 | rect_color_index = (rect_color_index % #rect_colors) + 1 74 | 75 | -- Recalculate the y position with the new direction 76 | rect_y = rect_y + rect_speed * rect_dir_y * delta ---@type number 77 | end 78 | 79 | -- Draw the rectangle 80 | draw_rectangle( 81 | window, 82 | math.floor(rect_x), 83 | math.floor(rect_y), 84 | rect_width, 85 | rect_height, 86 | rect_colors[rect_color_index] 87 | ) 88 | end 89 | -------------------------------------------------------------------------------- /demos/multi-window.lua: -------------------------------------------------------------------------------- 1 | local fenster = require('fenster') 2 | 3 | ---Draw a filled circle 4 | ---@param window window* 5 | ---@param x integer 6 | ---@param y integer 7 | ---@param radius integer 8 | ---@param color integer 9 | local function draw_circle(window, x, y, radius, color) 10 | local radius_neg = -radius 11 | local radius_pow2 = radius * radius 12 | for dy = radius_neg, radius do 13 | local dy_pow2 = dy * dy 14 | local sy = y + dy 15 | for dx = radius_neg, radius do 16 | if dx * dx + dy_pow2 < radius_pow2 then 17 | window:set(x + dx, sy, color) 18 | end 19 | end 20 | end 21 | end 22 | 23 | -- Open two windows 24 | local window_width = 426 25 | local window_height = 240 26 | local window1 = fenster.open( 27 | window_width, 28 | window_height, 29 | 'Multi-Window Demo - Press ESC to exit (1)' 30 | ) 31 | local window2 = fenster.open( 32 | window_width, 33 | window_height, 34 | 'Multi-Window Demo - Press ESC to exit (2)', 35 | 2 -- scale by 2 36 | ) 37 | 38 | -- Draw a circle on the both windows 39 | draw_circle( 40 | window1, 41 | math.floor(window_width / 2), 42 | math.floor(window_height / 2), 43 | 30, 44 | 0xff0000 45 | ) 46 | draw_circle( 47 | window2, 48 | math.floor(window_width / 2), 49 | math.floor(window_height / 2), 50 | 30, 51 | 0x0000ff 52 | ) 53 | 54 | -- Draw pixels on both windows 55 | while window1:loop() and window2:loop() and not window1.keys[27] and not window2.keys[27] do 56 | local x = math.random(0, window_width - 1) 57 | local y = math.random(0, window_height - 1) 58 | window1:set(x, y, 0xff0000) 59 | window2:set(x, y, 0x0000ff) 60 | end 61 | -------------------------------------------------------------------------------- /demos/noise.lua: -------------------------------------------------------------------------------- 1 | local fenster = require('fenster') 2 | 3 | -- Open a window 4 | local window_width = 256 5 | local window_height = 144 6 | local window_scale = 4 7 | local window = fenster.open( 8 | window_width, 9 | window_height, 10 | 'Noise Demo - Press ESC to exit', 11 | window_scale 12 | ) 13 | 14 | -- Generate noise 15 | while window:loop() and not window.keys[27] do 16 | for y = 0, window_height - 1 do 17 | for x = 0, window_width - 1 do 18 | window:set(x, y, math.random(0x000000, 0xffffff)) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /demos/paint.lua: -------------------------------------------------------------------------------- 1 | local fenster = require('fenster') 2 | 3 | ---Draw a line between two points (window boundary version) 4 | ---@param window window* 5 | ---@param x0 integer 6 | ---@param y0 integer 7 | ---@param x1 integer 8 | ---@param y1 integer 9 | ---@param color integer 10 | local function draw_line(window, x0, y0, x1, y1, color) 11 | local window_width = window.width 12 | local window_height = window.height 13 | local dx = math.abs(x1 - x0) 14 | local dy = math.abs(y1 - y0) 15 | local sx = x0 < x1 and 1 or -1 16 | local sy = y0 < y1 and 1 or -1 17 | local err = (dx > dy and dx or -dy) / 2 18 | local e2 ---@type number 19 | while true do 20 | -- NOTE: If you are copying this code, you may want to remove this boundary check 21 | if x0 >= 0 and x0 < window_width and y0 >= 0 and y0 < window_height then 22 | window:set(x0, y0, color) 23 | end 24 | 25 | if x0 == x1 and y0 == y1 then 26 | break 27 | end 28 | 29 | e2 = err 30 | if e2 > -dx then 31 | err = err - dy 32 | x0 = x0 + sx 33 | end 34 | if e2 < dy then 35 | err = err + dx 36 | y0 = y0 + sy 37 | end 38 | end 39 | end 40 | 41 | ---Fill an area with a specific color 42 | ---@param window window* 43 | ---@param x integer 44 | ---@param y integer 45 | ---@param color integer 46 | ---@param old_color integer? 47 | local function fill(window, x, y, color, old_color) 48 | old_color = old_color or window:get(x, y) 49 | if old_color == color then 50 | return 51 | end 52 | 53 | local window_width = window.width 54 | local window_height = window.height 55 | local stack = { (y * window_width) + x } 56 | while stack[1] do 57 | local pos = table.remove(stack) 58 | x = pos % window_width ---@type integer 59 | y = math.floor(pos / window_width) 60 | if window:get(x, y) == old_color then 61 | window:set(x, y, color) 62 | 63 | if x > 0 then 64 | stack[#stack + 1] = pos - 1 65 | end 66 | if x < window_width - 1 then 67 | stack[#stack + 1] = pos + 1 68 | end 69 | if y > 0 then 70 | stack[#stack + 1] = pos - window_width 71 | end 72 | if y < window_height - 1 then 73 | stack[#stack + 1] = pos + window_width 74 | end 75 | end 76 | end 77 | end 78 | 79 | -- Define the keyboard keys and their corresponding colors 80 | local key_color_map = { 81 | [48] = 0x000000, -- 0 (black) 82 | [49] = 0xffffff, -- 1 (white) 83 | [50] = 0x0000ff, -- 2 (blue) 84 | [51] = 0x00ff00, -- 3 (green) 85 | [52] = 0x00ffff, -- 4 (cyan) 86 | [53] = 0xff0000, -- 5 (red) 87 | [54] = 0xff00ff, -- 6 (magenta) 88 | [55] = 0xffff00, -- 7 (yellow) 89 | [56] = 0x808080, -- 8 (gray) 90 | [57] = 0xFFA500, -- 9 (orange) 91 | } 92 | 93 | -- Define the key for the fill action 94 | local fill_key = 70 -- F Key 95 | 96 | -- Open a window 97 | local window_width = 640 98 | local window_height = 360 99 | local window = fenster.open( 100 | window_width, 101 | window_height, 102 | 'Paint Demo - Press ESC to exit, 0-9 to change color, F to fill' 103 | ) 104 | 105 | -- Main loop 106 | local paint_color = key_color_map[49] 107 | local last_mouse_x ---@type integer? 108 | local last_mouse_y ---@type integer? 109 | while window:loop() and not window.keys[27] do 110 | local keys = window.keys 111 | 112 | -- Check if a color key is pressed 113 | for key, color in pairs(key_color_map) do 114 | if keys[key] then 115 | -- Set the color to the assigned key color 116 | paint_color = color 117 | end 118 | end 119 | 120 | local mouse_x = window.mousex 121 | local mouse_y = window.mousey 122 | local mouse_down = window.mousedown 123 | 124 | if mouse_down then 125 | -- Draw a line between the last mouse position and the current mouse position 126 | -- (Uses current mouse position if last mouse position is not set) 127 | draw_line( 128 | window, 129 | last_mouse_x or mouse_x, 130 | last_mouse_y or mouse_y, 131 | mouse_x, 132 | mouse_y, 133 | paint_color 134 | ) 135 | 136 | -- Update the last mouse position 137 | last_mouse_x, last_mouse_y = mouse_x, mouse_y 138 | elseif last_mouse_x and last_mouse_y then 139 | -- Reset the last mouse position 140 | last_mouse_x, last_mouse_y = nil, nil 141 | elseif keys[fill_key] then 142 | -- Fill the area at the mouse position 143 | fill(window, mouse_x, mouse_y, paint_color) 144 | 145 | -- Wait until fill key released 146 | while window:loop() and window.keys[fill_key] do 147 | -- 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /demos/plasma.lua: -------------------------------------------------------------------------------- 1 | local fenster = require('fenster') 2 | 3 | -- Open a window 4 | local window_width = 256 5 | local window_height = 144 6 | local window_scale = 4 7 | local window = fenster.open( 8 | window_width, 9 | window_height, 10 | 'Plasma Demo - Press ESC to exit', 11 | window_scale 12 | ) 13 | 14 | -- Calculate plasma effect steps 15 | local plasma_y_step = 1 / window_height 16 | local plasma_x_step = 1 / window_width 17 | 18 | -- Draw plasma effect 19 | local time = 0 20 | while window:loop() and not window.keys[27] do 21 | local py = 0 22 | for y = 1, window_height - 1 do 23 | local px = 0 24 | for x = 1, window_width - 1 do 25 | local k = 0.1 + math.cos(py + math.sin(0.148 - time)) + 2.4 * time 26 | local w = 0.9 + math.cos(px + math.cos(0.628 + time)) - 0.7 * time 27 | local d = math.sqrt(px * px + py * py) 28 | local s = 7.0 * math.cos(d + w) * math.sin(k + w) 29 | local r = math.floor((0.5 + 0.5 * math.cos(s + 0.2)) * 255) 30 | local g = math.floor((0.5 + 0.5 * math.cos(s + 0.5)) * 255) 31 | local b = math.floor((0.5 + 0.5 * math.cos(s + 0.7)) * 255) 32 | window:set(x, y, fenster.rgb(r, g, b)) 33 | 34 | px = px + plasma_x_step 35 | end 36 | 37 | py = py + plasma_y_step 38 | end 39 | 40 | time = time + 0.5 * window.delta 41 | end 42 | -------------------------------------------------------------------------------- /demos/rhodena-curve.lua: -------------------------------------------------------------------------------- 1 | -- Rhodonea Curves by Jakob Kruse 2 | -- https://github.com/jakob-kruse 3 | -- You can read more here https://en.wikipedia.org/wiki/Rose_(mathematics) 4 | 5 | local fenster = require('fenster') 6 | 7 | local windowSize = 900 8 | local window = fenster.open(windowSize, windowSize, 'Math Roses - R to reset + ESC to exit', 1, 0) 9 | 10 | ---@param delta number 11 | ---@param variant fun(delta:number):number 12 | ---@return number 13 | local function generateRoseValue(delta, variant) 14 | return 100 * math.sin(variant(delta)) 15 | end 16 | 17 | ---@param offsetX number 18 | ---@param offsetY number 19 | ---@param delta number 20 | ---@param variant fun(delta:number):number 21 | ---@param generator fun(delta:number,variant:fun(delta:number):number):number 22 | local function drawRose(offsetX, offsetY, delta, variant, generator) 23 | local radius = generator(delta, variant) 24 | local x = math.floor(radius * math.cos(delta) + offsetX) 25 | local y = math.floor(radius * math.sin(delta) + offsetY) 26 | 27 | x = math.max(0, math.min(windowSize - 1, x)) 28 | y = math.max(0, math.min(windowSize - 1, y)) 29 | 30 | window:set(x, y, 0xffffff) 31 | end 32 | 33 | local variantValues = { 34 | ---Circle 35 | ---@param delta number 36 | ---@return number 37 | function(delta) 38 | return delta / delta 39 | end, 40 | 41 | ---4-leaf rose 42 | ---@param delta number 43 | ---@return number 44 | function(delta) 45 | return math.sin(delta * 2) 46 | end, 47 | 48 | ---12-leaf rose 49 | ---@param delta number 50 | ---@return number 51 | function(delta) 52 | return math.sin(delta * 6) 53 | end, 54 | 55 | ---Butterfly 56 | ---@param delta number 57 | ---@return number 58 | function(delta) 59 | return math.sin(delta) + math.sin(delta * 4) 60 | end, 61 | 62 | ---Biker Glasses 63 | ---@param delta number 64 | ---@return number 65 | function(delta) 66 | return math.sin(delta) + math.sin(delta * 3) 67 | end, 68 | 69 | ---Neutron Star 70 | ---@param delta number 71 | ---@return number 72 | function(delta) 73 | return math.tan(delta) 74 | end, 75 | 76 | ---Logarithmic Spiral 77 | ---@param delta number 78 | ---@return number 79 | function(delta) 80 | return math.log(delta) 81 | end, 82 | 83 | ---@param delta number 84 | ---@return number 85 | function(delta) 86 | return math.rad(delta) * 10 87 | end, 88 | 89 | ---@param delta number 90 | ---@return number 91 | function(delta) 92 | return math.fmod(delta, 2) 93 | end, 94 | } 95 | 96 | local gridSize = math.ceil(math.sqrt(#variantValues)) 97 | local cellSize = windowSize / gridSize 98 | local delta = 1 99 | 100 | while window:loop() and not window.keys[27] do 101 | if window.keys[82] then 102 | delta = 1 103 | window:clear() 104 | end 105 | 106 | for i = 0, gridSize - 1 do 107 | for j = 0, gridSize - 1 do 108 | local variant = variantValues[i * gridSize + j + 1] 109 | if variant then 110 | drawRose( 111 | i * cellSize + cellSize / 2, 112 | j * cellSize + cellSize / 2, 113 | delta, 114 | variant, 115 | generateRoseValue 116 | ) 117 | end 118 | end 119 | end 120 | 121 | delta = delta + 0.0005 122 | end 123 | -------------------------------------------------------------------------------- /demos/text-editor.lua: -------------------------------------------------------------------------------- 1 | local fenster = require('fenster') 2 | 3 | -- Define the microknight font 4 | local microknight_font = { 5 | 0x00, 0x0c, 0x1b, 0x0d, 0x81, 0x03, 0x01, 0xc0, 0x30, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 6 | 0x00, 0x0c, 0x1b, 0x0d, 0x87, 0xc4, 0xb3, 0x60, 0x30, 0x30, 0x0c, 0x1b, 0x03, 0x00, 0x00, 0x00, 7 | 0x00, 0x0c, 0x09, 0x1f, 0xcd, 0x03, 0xe1, 0xc0, 0x10, 0x60, 0x06, 0x0e, 0x03, 0x00, 0x00, 0x00, 8 | 0x00, 0x0c, 0x00, 0x0d, 0x87, 0xc0, 0xc3, 0xd8, 0x20, 0x60, 0x06, 0x3f, 0x8f, 0xc0, 0x03, 0xe0, 9 | 0x00, 0x0c, 0x00, 0x1f, 0xc1, 0x61, 0x83, 0x70, 0x00, 0x60, 0x06, 0x0e, 0x03, 0x01, 0x80, 0x00, 10 | 0x00, 0x00, 0x00, 0x0d, 0x81, 0x63, 0x63, 0x60, 0x00, 0x30, 0x0c, 0x1b, 0x03, 0x01, 0x80, 0x00, 11 | 0x00, 0x0c, 0x00, 0x0d, 0x87, 0xc6, 0x91, 0xf0, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x80, 0x00, 12 | 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x60, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 13 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 14 | 0x00, 0x00, 0x00, 0x03, 0x07, 0xc1, 0xe0, 0x61, 0xf0, 0x70, 0x7f, 0x1e, 0x0f, 0x00, 0x00, 0x00, 15 | 0x00, 0x03, 0x1e, 0x03, 0x00, 0x60, 0x30, 0x61, 0x80, 0xc0, 0x03, 0x33, 0x19, 0x81, 0x80, 0xc0, 16 | 0x00, 0x06, 0x33, 0x07, 0x03, 0xc0, 0xe0, 0xc1, 0xf8, 0xfc, 0x06, 0x1f, 0x18, 0xc1, 0x80, 0xc0, 17 | 0x00, 0x0c, 0x37, 0x83, 0x06, 0x00, 0x31, 0xb0, 0x0c, 0xc6, 0x0c, 0x31, 0x98, 0xc0, 0x00, 0x00, 18 | 0x00, 0x18, 0x3d, 0x83, 0x0c, 0x02, 0x33, 0x30, 0x8c, 0xc6, 0x0c, 0x31, 0x8f, 0xc0, 0x00, 0x00, 19 | 0x18, 0x30, 0x39, 0x83, 0x0c, 0x06, 0x33, 0xf9, 0x98, 0xcc, 0x0c, 0x33, 0x00, 0xc1, 0x80, 0xc0, 20 | 0x18, 0x60, 0x1f, 0x0f, 0xcf, 0xe3, 0xe0, 0x30, 0xf0, 0x78, 0x0c, 0x1e, 0x03, 0x81, 0x80, 0x40, 21 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 22 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 23 | 0x00, 0x00, 0x00, 0x0f, 0x83, 0x83, 0xc3, 0xe0, 0xf0, 0xf8, 0x7f, 0x3f, 0x87, 0x0c, 0x63, 0xf0, 24 | 0x18, 0x00, 0x0c, 0x18, 0xc6, 0xc6, 0x63, 0x31, 0x98, 0xcc, 0x60, 0x30, 0x0c, 0x0c, 0x60, 0xc0, 25 | 0x30, 0x3e, 0x06, 0x00, 0xcd, 0xe6, 0x33, 0xf1, 0x80, 0xc6, 0x7e, 0x3f, 0x18, 0x0c, 0x60, 0xc0, 26 | 0x60, 0x00, 0x03, 0x07, 0x8f, 0x67, 0xf3, 0x19, 0x80, 0xc6, 0x60, 0x30, 0x19, 0xcf, 0xe0, 0xc0, 27 | 0x30, 0x3e, 0x06, 0x06, 0x0d, 0xe6, 0x33, 0x19, 0x80, 0xc6, 0x60, 0x30, 0x18, 0xcc, 0x60, 0xc0, 28 | 0x18, 0x00, 0x0c, 0x00, 0x0c, 0x06, 0x33, 0x31, 0x8c, 0xc6, 0x60, 0x30, 0x18, 0xcc, 0x60, 0xc0, 29 | 0x00, 0x00, 0x00, 0x06, 0x06, 0x66, 0x33, 0xe0, 0xf8, 0xfc, 0x7f, 0x30, 0x0f, 0xcc, 0x63, 0xf0, 30 | 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 31 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 32 | 0x0e, 0x63, 0x30, 0x18, 0xcc, 0x63, 0xc3, 0xe0, 0xf0, 0xf8, 0x3c, 0x1f, 0x98, 0xcc, 0x66, 0x30, 33 | 0x06, 0x66, 0x30, 0x1d, 0xce, 0x66, 0x63, 0x31, 0x98, 0xcc, 0x60, 0x06, 0x18, 0xcc, 0x66, 0x30, 34 | 0x06, 0x6c, 0x30, 0x1f, 0xcf, 0x66, 0x33, 0x19, 0x8c, 0xc6, 0x3e, 0x06, 0x18, 0xcc, 0x66, 0x30, 35 | 0x06, 0x78, 0x30, 0x1a, 0xcd, 0xe6, 0x33, 0x19, 0x8c, 0xc6, 0x03, 0x06, 0x18, 0xc6, 0xc6, 0xb0, 36 | 0xc6, 0x6c, 0x30, 0x18, 0xcc, 0xe6, 0x33, 0xf1, 0x8c, 0xfc, 0x23, 0x06, 0x18, 0xc6, 0xc7, 0xf0, 37 | 0xc6, 0x66, 0x30, 0x18, 0xcc, 0x66, 0x33, 0x01, 0xac, 0xd8, 0x63, 0x06, 0x18, 0xc3, 0x87, 0x70, 38 | 0x7c, 0x63, 0x3f, 0x98, 0xcc, 0x63, 0xe3, 0x00, 0xf8, 0xcc, 0x3e, 0x06, 0x0f, 0x83, 0x86, 0x30, 39 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 40 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 41 | 0xc6, 0x63, 0x3f, 0x87, 0x00, 0x01, 0xc0, 0x40, 0x00, 0x18, 0x00, 0x30, 0x00, 0x00, 0x60, 0x00, 42 | 0x6c, 0x63, 0x03, 0x06, 0x0c, 0x00, 0xc0, 0xe0, 0x00, 0x18, 0x1e, 0x3e, 0x0f, 0x03, 0xe3, 0xc0, 43 | 0x38, 0x63, 0x06, 0x06, 0x06, 0x00, 0xc1, 0xb0, 0x00, 0x10, 0x03, 0x33, 0x19, 0x86, 0x66, 0x60, 44 | 0x38, 0x3e, 0x0c, 0x06, 0x03, 0x00, 0xc0, 0x00, 0x00, 0x08, 0x3f, 0x31, 0x98, 0x0c, 0x67, 0xe0, 45 | 0x6c, 0x06, 0x18, 0x06, 0x01, 0x80, 0xc0, 0x00, 0x00, 0x00, 0x63, 0x31, 0x98, 0x0c, 0x66, 0x00, 46 | 0xc6, 0x06, 0x30, 0x06, 0x00, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x63, 0x31, 0x98, 0xcc, 0x66, 0x30, 47 | 0xc6, 0x06, 0x3f, 0x87, 0x00, 0x61, 0xc0, 0x00, 0x00, 0x00, 0x3f, 0x3f, 0x0f, 0x87, 0xe3, 0xe0, 48 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 49 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 50 | 0x38, 0x00, 0x30, 0x03, 0x00, 0xc6, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 51 | 0x6c, 0x3f, 0x3e, 0x00, 0x00, 0x06, 0x60, 0x61, 0x88, 0xf8, 0x3c, 0x3e, 0x07, 0xcf, 0xc3, 0xc0, 52 | 0x60, 0x63, 0x33, 0x07, 0x01, 0xc6, 0xc0, 0x61, 0xdc, 0xcc, 0x66, 0x33, 0x0c, 0xcc, 0x66, 0x00, 53 | 0x78, 0x63, 0x31, 0x83, 0x00, 0xc7, 0x80, 0x61, 0xfc, 0xc6, 0x63, 0x31, 0x98, 0xcc, 0x03, 0xe0, 54 | 0x60, 0x63, 0x31, 0x83, 0x00, 0xc6, 0xc0, 0x61, 0xac, 0xc6, 0x63, 0x31, 0x98, 0xcc, 0x00, 0x30, 55 | 0x60, 0x3f, 0x31, 0x83, 0x00, 0xc6, 0x60, 0x61, 0x8c, 0xc6, 0x63, 0x31, 0x98, 0xcc, 0x06, 0x30, 56 | 0x60, 0x03, 0x31, 0x8f, 0xc4, 0xc6, 0x31, 0xf9, 0x8c, 0xc6, 0x3e, 0x3f, 0x0f, 0xcc, 0x03, 0xe0, 57 | 0x60, 0x3e, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0xc0, 0x00, 0x00, 58 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 59 | 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x18, 0x1c, 0x87, 0x00, 0x00, 0xc0, 60 | 0x7c, 0x63, 0x31, 0x98, 0xcc, 0x66, 0x33, 0xf8, 0x30, 0x18, 0x0c, 0x27, 0x0e, 0x00, 0x00, 0x00, 61 | 0x30, 0x63, 0x31, 0x9a, 0xc6, 0xc6, 0x30, 0x30, 0x30, 0x18, 0x0c, 0x00, 0x1c, 0x00, 0x00, 0xc0, 62 | 0x30, 0x63, 0x1b, 0x1f, 0xc3, 0x86, 0x30, 0x60, 0x60, 0x18, 0x06, 0x00, 0x18, 0x20, 0x00, 0xc0, 63 | 0x30, 0x63, 0x1b, 0x0f, 0x83, 0x86, 0x30, 0xc0, 0x30, 0x18, 0x0c, 0x00, 0x10, 0x60, 0x00, 0xc0, 64 | 0x32, 0x63, 0x0e, 0x0d, 0x86, 0xc3, 0xf1, 0x80, 0x30, 0x18, 0x0c, 0x00, 0x00, 0xe0, 0x00, 0xc0, 65 | 0x1c, 0x3f, 0x0e, 0x08, 0x8c, 0x60, 0x33, 0xf8, 0x18, 0x18, 0x18, 0x00, 0x01, 0xc0, 0x00, 0xc0, 66 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x18, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 67 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 68 | 0x30, 0x1c, 0x00, 0x18, 0xc1, 0x83, 0xc1, 0xb0, 0x78, 0x00, 0x00, 0x1f, 0x00, 0x03, 0xc3, 0xe0, 69 | 0x78, 0x36, 0x31, 0x98, 0xc1, 0x86, 0x00, 0x00, 0x84, 0x7e, 0x1b, 0x03, 0x00, 0x04, 0x20, 0x00, 70 | 0xcc, 0x30, 0x1f, 0x18, 0xc1, 0x83, 0xe0, 0x01, 0x32, 0xc6, 0x36, 0x00, 0x00, 0x0b, 0x90, 0x00, 71 | 0xc0, 0x7c, 0x31, 0x8f, 0x80, 0x06, 0x30, 0x01, 0x42, 0xc6, 0x6c, 0x00, 0x0f, 0x8a, 0x50, 0x00, 72 | 0xc0, 0x30, 0x31, 0x81, 0x81, 0x86, 0x30, 0x01, 0x42, 0x7e, 0x36, 0x00, 0x00, 0x0b, 0x90, 0x00, 73 | 0xc6, 0x30, 0x1f, 0x07, 0xc1, 0x83, 0xe0, 0x01, 0x32, 0x00, 0x1b, 0x00, 0x00, 0x0a, 0x50, 0x00, 74 | 0x7c, 0x7f, 0x31, 0x81, 0x81, 0x80, 0x30, 0x00, 0x84, 0x7c, 0x00, 0x00, 0x00, 0x04, 0x20, 0x00, 75 | 0x30, 0x00, 0x00, 0x00, 0x00, 0x01, 0xe0, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x00, 76 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 77 | 0x38, 0x00, 0x1c, 0x0e, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x03, 0x00, 78 | 0x6c, 0x08, 0x06, 0x03, 0x03, 0x06, 0x31, 0xf8, 0x00, 0x00, 0x38, 0x1f, 0x1b, 0x0e, 0x67, 0x30, 79 | 0x6c, 0x3e, 0x0c, 0x06, 0x06, 0x06, 0x33, 0xd0, 0x30, 0x00, 0x18, 0x31, 0x8d, 0x86, 0xc3, 0x60, 80 | 0x38, 0x08, 0x18, 0x03, 0x00, 0x06, 0x31, 0xd0, 0x30, 0x00, 0x18, 0x31, 0x86, 0xc7, 0xa3, 0xc0, 81 | 0x00, 0x00, 0x1e, 0x0e, 0x00, 0x06, 0x30, 0x50, 0x00, 0x00, 0x3c, 0x1f, 0x0d, 0x83, 0x61, 0xf0, 82 | 0x00, 0x3e, 0x00, 0x00, 0x00, 0x06, 0x30, 0x50, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x06, 0xf3, 0x18, 83 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xe0, 0x50, 0x00, 0x18, 0x00, 0x1f, 0x00, 0x0c, 0xf6, 0x70, 84 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x30, 0xf8, 85 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 86 | 0xe0, 0x18, 0x0c, 0x03, 0x03, 0x03, 0x91, 0xb0, 0xf0, 0x3f, 0x3c, 0x0c, 0x03, 0x01, 0x83, 0x60, 87 | 0x36, 0x00, 0x02, 0x04, 0x0c, 0xc4, 0xe0, 0x01, 0x98, 0x6c, 0x66, 0x02, 0x04, 0x06, 0x60, 0x00, 88 | 0x6c, 0x18, 0x1e, 0x0f, 0x07, 0x83, 0xc1, 0xe0, 0xf0, 0xcf, 0x60, 0x3f, 0x9f, 0xcf, 0xe7, 0xf0, 89 | 0x3a, 0x1e, 0x33, 0x19, 0x8c, 0xc6, 0x63, 0x31, 0x98, 0xfc, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x00, 90 | 0xf6, 0x03, 0x3f, 0x9f, 0xcf, 0xe7, 0xf3, 0xf9, 0xfc, 0xcc, 0x60, 0x3f, 0x1f, 0x8f, 0xc7, 0xe0, 91 | 0x6f, 0x63, 0x31, 0x98, 0xcc, 0x66, 0x33, 0x19, 0x8c, 0xcc, 0x63, 0x30, 0x18, 0x0c, 0x06, 0x00, 92 | 0xcf, 0x3e, 0x31, 0x98, 0xcc, 0x66, 0x33, 0x19, 0x8c, 0xcf, 0x3e, 0x3f, 0x9f, 0xcf, 0xe7, 0xf0, 93 | 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 94 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 95 | 0x30, 0x0c, 0x06, 0x0d, 0x8f, 0x83, 0x90, 0xc0, 0x30, 0x30, 0x39, 0x1b, 0x00, 0x07, 0x81, 0x80, 96 | 0x08, 0x10, 0x19, 0x80, 0x0c, 0xc4, 0xe0, 0x20, 0x40, 0xcc, 0x4e, 0x00, 0x0f, 0x8c, 0xc0, 0x40, 97 | 0x7e, 0x3f, 0x1f, 0x8f, 0xcc, 0x67, 0x31, 0xe0, 0xf0, 0x78, 0x3c, 0x1e, 0x1a, 0xcd, 0xe6, 0x30, 98 | 0x18, 0x0c, 0x06, 0x03, 0x0e, 0x67, 0xb3, 0x31, 0x98, 0xcc, 0x66, 0x33, 0x1f, 0xef, 0x66, 0x30, 99 | 0x18, 0x0c, 0x06, 0x03, 0x0c, 0x66, 0xf3, 0x19, 0x8c, 0xc6, 0x63, 0x31, 0x9b, 0x6e, 0x66, 0x30, 100 | 0x18, 0x0c, 0x06, 0x03, 0x0c, 0x66, 0x73, 0x19, 0x8c, 0xc6, 0x63, 0x31, 0x98, 0xec, 0x66, 0x30, 101 | 0x7e, 0x3f, 0x1f, 0x8f, 0xcf, 0xc6, 0x31, 0xf0, 0xf8, 0x7c, 0x3e, 0x1f, 0x0f, 0xc7, 0xc3, 0xe0, 102 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 103 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 104 | 0x18, 0x0c, 0x1b, 0x03, 0x0c, 0x00, 0x00, 0xc0, 0x30, 0x18, 0x39, 0x1b, 0x07, 0x80, 0x00, 0x00, 105 | 0x20, 0x33, 0x00, 0x04, 0x0f, 0x83, 0xc0, 0x20, 0x40, 0x66, 0x4e, 0x00, 0x0c, 0xc7, 0xe3, 0xc0, 106 | 0xc6, 0x63, 0x31, 0x98, 0xcc, 0xc6, 0x60, 0xf0, 0x78, 0x3c, 0x1e, 0x0f, 0x07, 0x81, 0xb6, 0x60, 107 | 0xc6, 0x63, 0x31, 0x98, 0xcc, 0x66, 0xe0, 0x18, 0x0c, 0x06, 0x03, 0x01, 0x80, 0xc7, 0xf6, 0x00, 108 | 0xc6, 0x63, 0x31, 0x8f, 0x8f, 0xc6, 0x31, 0xf8, 0xfc, 0x7e, 0x3f, 0x1f, 0x8f, 0xcd, 0x86, 0x00, 109 | 0xc6, 0x63, 0x31, 0x81, 0x8c, 0x06, 0x33, 0x19, 0x8c, 0xc6, 0x63, 0x31, 0x98, 0xcd, 0x86, 0x30, 110 | 0x7c, 0x3e, 0x1f, 0x01, 0x8c, 0x06, 0xe1, 0xf8, 0xfc, 0x7e, 0x3f, 0x1f, 0x8f, 0xc7, 0xf3, 0xe0, 111 | 0x00, 0x00, 0x00, 0x00, 0x0c, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 112 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 113 | 0x30, 0x0c, 0x0c, 0x0d, 0x83, 0x00, 0xc0, 0x60, 0xd8, 0x0c, 0x39, 0x0c, 0x03, 0x01, 0x83, 0x90, 114 | 0x08, 0x10, 0x33, 0x00, 0x00, 0x81, 0x01, 0x98, 0x00, 0x16, 0x4e, 0x02, 0x04, 0x06, 0x64, 0xe0, 115 | 0x78, 0x3c, 0x1e, 0x0f, 0x03, 0x81, 0xc0, 0xe0, 0x70, 0x3e, 0x7c, 0x1e, 0x0f, 0x07, 0x83, 0xc0, 116 | 0xfc, 0x7e, 0x3f, 0x1f, 0x81, 0x80, 0xc0, 0x60, 0x30, 0x66, 0x66, 0x33, 0x19, 0x8c, 0xc6, 0x60, 117 | 0xc0, 0x60, 0x30, 0x18, 0x01, 0x80, 0xc0, 0x60, 0x30, 0xc6, 0x63, 0x31, 0x98, 0xcc, 0x66, 0x30, 118 | 0xc6, 0x63, 0x31, 0x98, 0xc1, 0x80, 0xc0, 0x60, 0x30, 0xc6, 0x63, 0x31, 0x98, 0xcc, 0x66, 0x30, 119 | 0x7c, 0x3e, 0x1f, 0x0f, 0x87, 0xe3, 0xf1, 0xf8, 0xfc, 0x7e, 0x63, 0x1f, 0x0f, 0x87, 0xc3, 0xe0, 120 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 121 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 122 | 0x6c, 0x00, 0x00, 0x06, 0x01, 0x80, 0xc1, 0xb0, 0x30, 0xc0, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 123 | 0x00, 0x0c, 0x1e, 0x01, 0x02, 0x03, 0x30, 0x00, 0x40, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 124 | 0x78, 0x00, 0x33, 0x18, 0xcc, 0x66, 0x33, 0x19, 0x8c, 0xf8, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 125 | 0xcc, 0x3f, 0x37, 0x98, 0xcc, 0x66, 0x33, 0x19, 0x8c, 0xcc, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 126 | 0xc6, 0x00, 0x3d, 0x98, 0xcc, 0x66, 0x33, 0x19, 0x8c, 0xc6, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 127 | 0xc6, 0x0c, 0x39, 0x98, 0xcc, 0x66, 0x33, 0x18, 0xfc, 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 128 | 0x7c, 0x00, 0x1f, 0x0f, 0xc7, 0xe3, 0xf1, 0xf8, 0x0c, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 129 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xc0, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 130 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 131 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 132 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 133 | } 134 | 135 | -- Define the microknight layout 136 | local microknight_layout = { 137 | { 0, 0 }, { 9, 0 }, { 18, 0 }, { 27, 0 }, { 36, 0 }, { 45, 0 }, { 54, 0 }, { 63, 0 }, { 72, 0 }, { 81, 0 }, { 90, 0 }, 138 | { 99, 0 }, { 108, 0 }, { 117, 0 }, { 0, 9 }, { 9, 9 }, { 18, 9 }, { 27, 9 }, { 36, 9 }, { 45, 9 }, { 54, 9 }, 139 | { 63, 9 }, { 72, 9 }, { 81, 9 }, { 90, 9 }, { 99, 9 }, { 108, 9 }, { 117, 9 }, { 0, 18 }, { 9, 18 }, { 18, 18 }, 140 | { 27, 18 }, { 36, 18 }, { 45, 18 }, { 54, 18 }, { 63, 18 }, { 72, 18 }, { 81, 18 }, { 90, 18 }, { 99, 18 }, 141 | { 108, 18 }, { 117, 18 }, { 0, 27 }, { 9, 27 }, { 18, 27 }, { 27, 27 }, { 36, 27 }, { 45, 27 }, { 54, 27 }, { 63, 27 }, 142 | { 72, 27 }, { 81, 27 }, { 90, 27 }, { 99, 27 }, { 108, 27 }, { 117, 27 }, { 0, 36 }, { 9, 36 }, { 18, 36 }, 143 | { 27, 36 }, { 36, 36 }, { 45, 36 }, { 54, 36 }, { 63, 36 }, { 72, 36 }, { 81, 36 }, { 90, 36 }, { 99, 36 }, 144 | { 108, 36 }, { 117, 36 }, { 0, 45 }, { 9, 45 }, { 18, 45 }, { 27, 45 }, { 36, 45 }, { 45, 45 }, { 54, 45 }, 145 | { 63, 45 }, { 72, 45 }, { 81, 45 }, { 90, 45 }, { 99, 45 }, { 108, 45 }, { 117, 45 }, { 0, 54 }, { 9, 54 }, { 18, 54 }, 146 | { 27, 54 }, { 36, 54 }, { 45, 54 }, { 54, 54 }, { 63, 54 }, 147 | { 72, 54 }, { 81, 54 }, { 90, 54 }, { 99, 54 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, 148 | { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, 149 | { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, 150 | { 0, 0 }, { 0, 0 }, { 108, 54 }, { 117, 54 }, { 0, 63 }, { 9, 63 }, { 18, 63 }, { 27, 63 }, { 36, 63 }, { 45, 63 }, 151 | { 54, 63 }, { 63, 63 }, { 72, 63 }, { 81, 63 }, { 90, 63 }, { 99, 63 }, { 108, 63 }, { 117, 63 }, { 0, 72 }, { 9, 72 }, 152 | { 18, 72 }, { 27, 72 }, { 36, 72 }, { 45, 72 }, { 54, 72 }, { 63, 72 }, { 72, 72 }, { 81, 72 }, { 90, 72 }, { 99, 72 }, 153 | { 108, 72 }, { 117, 72 }, { 0, 81 }, { 9, 81 }, { 18, 81 }, { 27, 81 }, { 36, 81 }, { 45, 81 }, { 54, 81 }, { 63, 81 }, 154 | { 72, 81 }, { 81, 81 }, { 90, 81 }, { 99, 81 }, { 108, 81 }, { 117, 81 }, { 0, 90 }, { 9, 90 }, { 18, 90 }, 155 | { 27, 90 }, { 36, 90 }, { 45, 90 }, { 54, 90 }, { 63, 90 }, { 72, 90 }, { 81, 90 }, { 90, 90 }, { 99, 90 }, 156 | { 108, 90 }, { 117, 90 }, { 0, 99 }, { 9, 99 }, { 18, 99 }, { 27, 99 }, { 36, 99 }, { 45, 99 }, { 54, 99 }, { 63, 99 }, 157 | { 72, 99 }, { 81, 99 }, { 90, 99 }, { 99, 99 }, { 108, 99 }, { 117, 99 }, { 0, 108 }, { 9, 108 }, { 18, 108 }, 158 | { 27, 108 }, { 36, 108 }, { 45, 108 }, { 54, 108 }, { 63, 108 }, { 72, 108 }, { 81, 108 }, { 90, 108 }, { 99, 108 }, 159 | { 108, 108 }, { 117, 108 }, { 0, 117 }, { 9, 117 }, { 18, 117 }, { 27, 117 }, { 36, 117 }, { 45, 117 }, { 54, 117 }, 160 | { 63, 117 }, { 72, 117 }, { 81, 117 }, 161 | } 162 | 163 | -- Calculate the microknight texture 164 | local microknight_texture = {} ---@type integer[] 165 | for _, t in ipairs(microknight_font) do 166 | local bits = {} ---@type number[] 167 | for b = 8, 1, -1 do 168 | bits[b] = math.fmod(t, 2) 169 | t = math.floor((t - bits[b]) / 2) 170 | end 171 | 172 | microknight_texture[#microknight_texture + 1] = bits[1] == 0 and 0x000000 or 0xffffff 173 | microknight_texture[#microknight_texture + 1] = bits[2] == 0 and 0x000000 or 0xffffff 174 | microknight_texture[#microknight_texture + 1] = bits[3] == 0 and 0x000000 or 0xffffff 175 | microknight_texture[#microknight_texture + 1] = bits[4] == 0 and 0x000000 or 0xffffff 176 | microknight_texture[#microknight_texture + 1] = bits[5] == 0 and 0x000000 or 0xffffff 177 | microknight_texture[#microknight_texture + 1] = bits[6] == 0 and 0x000000 or 0xffffff 178 | microknight_texture[#microknight_texture + 1] = bits[7] == 0 and 0x000000 or 0xffffff 179 | microknight_texture[#microknight_texture + 1] = bits[8] == 0 and 0x000000 or 0xffffff 180 | end 181 | 182 | -- Define the height/width of each microknight character 183 | local microknight_size = 8 184 | 185 | ---Draw text using the microknight font (8x8 pixels per character) 186 | ---@param window window* 187 | ---@param text string 188 | ---@param x integer 189 | ---@param y integer 190 | local function draw_text(window, text, x, y) 191 | for character in text:gmatch('.') do 192 | local index = character:byte() - 31 193 | if index > #microknight_layout then 194 | index = 0 195 | end 196 | 197 | local layout = microknight_layout[index] 198 | local texture_offset = (layout[2] * 128) + layout[1] 199 | 200 | for fy = 0, microknight_size do 201 | for fx = 0, microknight_size do 202 | local texture_index = texture_offset + (fy * 128) + fx 203 | window:set( 204 | x + fx, 205 | y + fy, 206 | microknight_texture[texture_index + 1] 207 | ) 208 | end 209 | end 210 | 211 | x = x + microknight_size 212 | end 213 | end 214 | 215 | -- Define the key mappings 216 | local escape_key = 27 217 | local enter_key = 10 218 | local backspace_key = 8 219 | local character_keys = { 220 | [' '] = 32, 221 | ["'"] = 39, 222 | [','] = 44, 223 | ['-'] = 45, 224 | ['.'] = 46, 225 | ['/'] = 47, 226 | ['0'] = 48, 227 | ['1'] = 49, 228 | ['2'] = 50, 229 | ['3'] = 51, 230 | ['4'] = 52, 231 | ['5'] = 53, 232 | ['6'] = 54, 233 | ['7'] = 55, 234 | ['8'] = 56, 235 | ['9'] = 57, 236 | [';'] = 59, 237 | ['='] = 61, 238 | a = 65, 239 | b = 66, 240 | c = 67, 241 | d = 68, 242 | e = 69, 243 | f = 70, 244 | g = 71, 245 | h = 72, 246 | i = 73, 247 | j = 74, 248 | k = 75, 249 | l = 76, 250 | m = 77, 251 | n = 78, 252 | o = 79, 253 | p = 80, 254 | q = 81, 255 | r = 82, 256 | s = 83, 257 | t = 84, 258 | u = 85, 259 | v = 86, 260 | w = 87, 261 | x = 88, 262 | y = 89, 263 | z = 90, 264 | ['['] = 91, 265 | ['\\'] = 92, 266 | [']'] = 93, 267 | ['`'] = 96, 268 | } 269 | local character_uppercase_map = { 270 | ["'"] = '"', 271 | [','] = '<', 272 | ['-'] = '_', 273 | ['.'] = '>', 274 | ['/'] = '?', 275 | ['1'] = '!', 276 | ['2'] = '@', 277 | ['3'] = '#', 278 | ['4'] = '$', 279 | ['5'] = '%', 280 | ['6'] = '^', 281 | ['7'] = '&', 282 | ['8'] = '*', 283 | ['9'] = '(', 284 | ['0'] = ')', 285 | [';'] = ':', 286 | ['='] = '+', 287 | ['['] = '{', 288 | ['\\'] = '|', 289 | [']'] = '}', 290 | ['`'] = '~', 291 | } 292 | 293 | -- Text settings 294 | local text_offset_x = 16 295 | local text_offset_y = 16 296 | local text_line_height = microknight_size * 2 297 | local max_text_line_length = 80 -- should be higher than 30 298 | local max_text_lines = 30 299 | 300 | -- Open a window 301 | local window_width = text_offset_x + microknight_size * max_text_line_length + 302 | text_offset_x 303 | local window_height = text_offset_y + text_line_height * (max_text_lines - 1) + 304 | microknight_size + text_offset_y 305 | local window = fenster.open(window_width, window_height, 'Text Demo') 306 | 307 | -- Draw instructions 308 | draw_text( 309 | window, 310 | 'Type anything' 311 | .. string.rep(' ', max_text_line_length - 30) 312 | .. 'Press ESC to exit', 313 | text_offset_x, 314 | text_offset_y + text_line_height * 0 315 | ) 316 | draw_text( 317 | window, 318 | string.rep('-', max_text_line_length), 319 | text_offset_x, 320 | text_offset_y + text_line_height * 1 321 | ) 322 | 323 | -- Main loop 324 | local typed_text_lines = { '' } 325 | local curr_line = 1 326 | while window:loop() and not window.keys[escape_key] do 327 | local keys = window.keys 328 | 329 | -- Get text input and draw the new text when needed 330 | local pressed_key ---@type integer 331 | if keys[enter_key] then 332 | pressed_key = enter_key 333 | 334 | -- Check if we are under the line limit 335 | if curr_line + 1 < max_text_lines - 1 then 336 | -- Go to the next line and initialize if needed 337 | curr_line = curr_line + 1 338 | typed_text_lines[curr_line] = typed_text_lines[curr_line] or '' 339 | end 340 | elseif keys[backspace_key] then 341 | pressed_key = backspace_key 342 | 343 | -- Check if we are at the first position any line under the first 344 | if #typed_text_lines[curr_line] == 0 and curr_line > 1 then 345 | -- Return to the previous line 346 | curr_line = curr_line - 1 347 | else 348 | -- Remove last character from text 349 | typed_text_lines[curr_line] = typed_text_lines[curr_line]:sub(1, -2) 350 | 351 | -- Clear last character from window 352 | local start_x = text_offset_x + #typed_text_lines[curr_line] * microknight_size 353 | local start_y = text_offset_y + text_line_height * (curr_line + 1) 354 | for y = start_y, start_y + microknight_size do 355 | for x = start_x, start_x + microknight_size do 356 | window:set(x, y, 0x000000) 357 | end 358 | end 359 | end 360 | else 361 | -- Loop through all character keys (keys that add text) 362 | for key, code in pairs(character_keys) do 363 | if keys[code] then 364 | pressed_key = code 365 | 366 | -- Check if line is at max length 367 | if #typed_text_lines[curr_line] + 1 > max_text_line_length then 368 | break 369 | end 370 | 371 | -- Get the character to print 372 | local character = window.modshift 373 | and (character_uppercase_map[key] or key:upper()) 374 | or key 375 | 376 | -- Draw new character 377 | draw_text( 378 | window, 379 | character, 380 | text_offset_x + #typed_text_lines[curr_line] * microknight_size, 381 | text_offset_y + text_line_height * (curr_line + 1) 382 | ) 383 | 384 | -- Add new character to text line 385 | typed_text_lines[curr_line] = typed_text_lines[curr_line] .. character 386 | break 387 | end 388 | end 389 | end 390 | 391 | -- Wait until pressed key was released 392 | if pressed_key then 393 | while window:loop() and window.keys[pressed_key] do 394 | -- 395 | end 396 | end 397 | end 398 | -------------------------------------------------------------------------------- /demos/tunnel.lua: -------------------------------------------------------------------------------- 1 | -- Inspired by https://lodev.org/cgtutor/tunnel.html 2 | 3 | local fenster = require('fenster') 4 | 5 | local math = math 6 | -- Lua 5.3+ compatibility (math.atan2 can be replaced with math.atan) 7 | local atan2 = math.atan2 or math.atan 8 | 9 | ---Returns the bitwise XOR of two numbers 10 | ---(added for cross-Lua-version compatibility) 11 | ---@param a number 12 | ---@param b number 13 | ---@return integer 14 | local function xor(a, b) 15 | local result = 0 16 | local bitval = 1 17 | while a > 0 or b > 0 do 18 | if a % 2 ~= b % 2 then 19 | result = result + bitval 20 | end 21 | bitval = bitval * 2 22 | a = math.floor(a / 2) 23 | b = math.floor(b / 2) 24 | end 25 | return result 26 | end 27 | 28 | -- Open a window 29 | local window_width = 256 30 | local window_height = 144 31 | local window_scale = 4 32 | local window = fenster.open( 33 | window_width, 34 | window_height, 35 | 'Tunnel Demo - Press ESC to exit', 36 | window_scale 37 | ) 38 | 39 | -- Generate texture 40 | local texture_width = 256 41 | local texture_height = 256 42 | local texture = {} ---@type integer[][] 43 | for y = 0, texture_height - 1 do 44 | texture[y] = {} 45 | for x = 0, texture_width - 1 do 46 | texture[y][x] = xor((x * 256 / texture_width), (y * 256 / texture_height)) 47 | end 48 | end 49 | 50 | -- Generate non-linear transformation table 51 | local ratio = 32 52 | local distance_table = {} ---@type integer[][] 53 | local angle_table = {} ---@type integer[][] 54 | for y = 0, (window_height * 2) - 1 do 55 | distance_table[y] = {} 56 | angle_table[y] = {} 57 | for x = 0, (window_width * 2) - 1 do 58 | local distance = math.floor( 59 | ratio * texture_height / math.sqrt( 60 | (x - window_width) * (x - window_width) + (y - window_height) * (y - window_height) 61 | ) 62 | ) % texture_height 63 | if distance ~= distance then 64 | distance = 0 -- fix nan/inf values 65 | end 66 | distance_table[y][x] = distance 67 | 68 | local angle = math.floor(0.5 * texture_width * atan2(y - window_height, x - window_width) / math.pi) 69 | if angle ~= angle then 70 | angle = 0 -- fix nan/inf values 71 | end 72 | angle_table[y][x] = angle 73 | end 74 | end 75 | 76 | -- Begin the main loop 77 | local time = 0 78 | local half_window_width = window_width / 2 79 | local half_window_height = window_height / 2 80 | while window:loop() and not window.keys[27] do 81 | -- Calculate the shift values out of the animation value 82 | local shift_x = math.floor(texture_width * 1 * time); 83 | local shift_y = math.floor(texture_height * 0.25 * time); 84 | 85 | -- Calculate the look values out of the animation value 86 | -- (by using sine functions, it'll alternate between looking left/right and up/down) 87 | local shift_look_x = half_window_width + math.floor(half_window_width * math.sin(time)) 88 | local shift_look_y = half_window_height + math.floor(half_window_height * math.sin(time * 2)) 89 | 90 | for y = 0, window_height - 1 do 91 | for x = 0, window_width - 1 do 92 | -- Get the texel from the texture by using the tables, shifted with the animation variable 93 | -- (x and y are shifted as well with the "look" animation values) 94 | local texture_x = math.floor(distance_table[y + shift_look_y][x + shift_look_x] + shift_x) % texture_width 95 | local texture_y = math.floor(angle_table[y + shift_look_y][x + shift_look_x] + shift_y) % texture_height 96 | 97 | window:set(x, y, texture[texture_y][texture_x]) 98 | end 99 | end 100 | 101 | time = time + 0.5 * window.delta 102 | end 103 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | name: lua-fenster 2 | 3 | 4 | x-build-preset: &build_preset 5 | dockerfile_inline: |- 6 | ARG BASE_IMAGE=nickblah/lua:5.4-luarocks 7 | FROM $${BASE_IMAGE} 8 | 9 | SHELL ["/bin/bash", "-euxo", "pipefail", "-c"] 10 | RUN IFS=$$' \n\t'; \ 11 | \ 12 | echo '**** install system packages ****'; \ 13 | apt-get update; \ 14 | apt-get install --no-install-recommends --yes \ 15 | build-essential \ 16 | libx11-dev \ 17 | xvfb \ 18 | xauth \ 19 | ; \ 20 | echo '**** install luarocks packages ****'; \ 21 | luarocks install --no-doc \ 22 | busted \ 23 | ; \ 24 | echo '**** cleanup ****'; \ 25 | rm -rf \ 26 | /tmp/* \ 27 | /var/lib/apt/lists/* \ 28 | /var/tmp/* \ 29 | /var/cache/luarocks/* 30 | 31 | WORKDIR /project 32 | COPY . . 33 | 34 | ENTRYPOINT ["/bin/bash", "-euxo", "pipefail", "-c"] 35 | 36 | 37 | x-service-preset: &service_preset 38 | restart: no 39 | environment: 40 | TZ: ${TZ:-${TIMEZONE:-Europe/Vienna}} 41 | command: |- 42 | ' 43 | luarocks make 44 | trap "cat /tmp/xvfb-run-error.txt && rm --force /tmp/xvfb-run-error.txt" EXIT INT TERM QUIT 45 | xvfb-run --auto-servernum --error-file=/tmp/xvfb-run-error.txt luarocks test -- --verbose 46 | ' 47 | 48 | 49 | services: 50 | lua-5_1: 51 | <<: *service_preset 52 | build: 53 | <<: *build_preset 54 | args: 55 | BASE_IMAGE: nickblah/lua:5.1-luarocks 56 | 57 | lua-5_2: 58 | <<: *service_preset 59 | depends_on: 60 | lua-5_1: 61 | condition: service_completed_successfully 62 | build: 63 | <<: *build_preset 64 | args: 65 | BASE_IMAGE: nickblah/lua:5.2-luarocks 66 | 67 | lua-5_3: 68 | <<: *service_preset 69 | depends_on: 70 | lua-5_2: 71 | condition: service_completed_successfully 72 | build: 73 | <<: *build_preset 74 | args: 75 | BASE_IMAGE: nickblah/lua:5.3-luarocks 76 | 77 | lua-5_4: 78 | <<: *service_preset 79 | depends_on: 80 | lua-5_3: 81 | condition: service_completed_successfully 82 | build: 83 | <<: *build_preset 84 | args: 85 | BASE_IMAGE: nickblah/lua:5.4-luarocks 86 | 87 | luajit-2_0: 88 | <<: *service_preset 89 | depends_on: 90 | lua-5_4: 91 | condition: service_completed_successfully 92 | build: 93 | <<: *build_preset 94 | args: 95 | BASE_IMAGE: nickblah/luajit:2.0-luarocks 96 | 97 | luajit-2_1: 98 | <<: *service_preset 99 | depends_on: 100 | luajit-2_0: 101 | condition: service_completed_successfully 102 | build: 103 | <<: *build_preset 104 | args: 105 | BASE_IMAGE: nickblah/luajit:2.1-luarocks 106 | 107 | luajit-2_0-compat: 108 | <<: *service_preset 109 | depends_on: 110 | luajit-2_1: 111 | condition: service_completed_successfully 112 | build: 113 | <<: *build_preset 114 | args: 115 | BASE_IMAGE: nickblah/luajit:2.0-lua52compat-luarocks 116 | 117 | luajit-2_1-compat: 118 | <<: *service_preset 119 | depends_on: 120 | luajit-2_0-compat: 121 | condition: service_completed_successfully 122 | build: 123 | <<: *build_preset 124 | args: 125 | BASE_IMAGE: nickblah/luajit:2.1-lua52compat-luarocks 126 | -------------------------------------------------------------------------------- /fenster-dev-1.rockspec: -------------------------------------------------------------------------------- 1 | rockspec_format = '3.0' 2 | package = 'fenster' 3 | version = 'dev-1' -- this will be replaced by the release workflow 4 | source = { 5 | url = 'git+https://github.com/jonasgeiler/lua-fenster', 6 | branch = 'main', -- this will be replaced by the release workflow 7 | } 8 | description = { 9 | summary = 'The most minimal cross-platform GUI library - now in Lua!', 10 | detailed = '' .. 11 | 'A Lua binding for the fenster (https://github.com/zserge/fenster) ' .. 12 | 'C library, providing a minimal cross-platform GUI library for ' .. 13 | 'creating windows and drawing pixels.', 14 | license = 'MIT', 15 | homepage = 'https://github.com/jonasgeiler/lua-fenster', 16 | issues_url = 'https://github.com/jonasgeiler/lua-fenster/issues', 17 | maintainer = 'Jonas Geiler', 18 | labels = { 19 | 'gui', 'graphics', 'pixel', 'windowing', '2d', 'drawing', 'window', 20 | 'framebuffer', 'gui-library', 21 | }, 22 | } 23 | dependencies = { 24 | 'lua >= 5.1, <= 5.4', 25 | } 26 | build_dependencies = { 27 | platforms = { 28 | macosx = { 29 | 'luarocks-build-extended', 30 | }, 31 | }, 32 | } 33 | external_dependencies = { 34 | platforms = { 35 | linux = { 36 | X11 = { 37 | library = 'X11', 38 | }, 39 | }, 40 | win32 = { 41 | GDI32 = { 42 | library = 'gdi32', 43 | }, 44 | USER32 = { 45 | library = 'user32', 46 | }, 47 | }, 48 | }, 49 | } 50 | build = { 51 | type = 'builtin', 52 | modules = { 53 | fenster = { 54 | sources = 'src/main.c', 55 | }, 56 | }, 57 | platforms = { 58 | linux = { 59 | modules = { 60 | fenster = { 61 | libraries = { 62 | 'X11', 63 | }, 64 | incdirs = { 65 | '$(X11_INCDIR)', 66 | }, 67 | libdirs = { 68 | '$(X11_LIBDIR)', 69 | }, 70 | }, 71 | }, 72 | }, 73 | win32 = { 74 | modules = { 75 | fenster = { 76 | libraries = { 77 | 'gdi32', 78 | 'user32', 79 | }, 80 | incdirs = { 81 | '$(GDI32_INCDIR)', 82 | '$(USER32_INCDIR)', 83 | }, 84 | libdirs = { 85 | '$(GDI32_LIBDIR)', 86 | '$(USER32_LIBDIR)', 87 | }, 88 | }, 89 | }, 90 | }, 91 | macosx = { 92 | type = 'extended', 93 | modules = { 94 | fenster = { 95 | variables = { 96 | LIBFLAG_EXTRAS = { 97 | '-framework', 'Cocoa', 98 | }, 99 | }, 100 | }, 101 | }, 102 | }, 103 | }, 104 | } 105 | test = { 106 | type = 'busted', 107 | flags = '--verbose', 108 | } 109 | -------------------------------------------------------------------------------- /include/main.h: -------------------------------------------------------------------------------- 1 | #ifndef FENSTER_MAIN_H 2 | #define FENSTER_MAIN_H 3 | 4 | #include 5 | 6 | #ifdef _WIN32 7 | #define FENSTER_EXPORT __declspec(dllexport) 8 | #else 9 | #define FENSTER_EXPORT extern 10 | #endif 11 | 12 | #ifdef __cplusplus 13 | extern "C" { 14 | #endif 15 | 16 | FENSTER_EXPORT int luaopen_fenster(lua_State *L); 17 | 18 | #ifdef __cplusplus 19 | } 20 | #endif 21 | 22 | #endif // FENSTER_MAIN_H 23 | -------------------------------------------------------------------------------- /lib/compat-5.3/compat-5.3.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "compat-5.3.h" 8 | 9 | /* don't compile it again if it already is included via compat53.h */ 10 | #ifndef COMPAT53_C_ 11 | #define COMPAT53_C_ 12 | 13 | 14 | 15 | /* definitions for Lua 5.1 only */ 16 | #if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM == 501 17 | 18 | #ifndef COMPAT53_FOPEN_NO_LOCK 19 | # if defined(_MSC_VER) 20 | # define COMPAT53_FOPEN_NO_LOCK 1 21 | # else /* otherwise */ 22 | # define COMPAT53_FOPEN_NO_LOCK 0 23 | # endif /* VC++ only so far */ 24 | #endif /* No-lock fopen_s usage if possible */ 25 | 26 | #if defined(_MSC_VER) && COMPAT53_FOPEN_NO_LOCK 27 | # include 28 | #endif /* VC++ _fsopen for share-allowed file read */ 29 | 30 | #ifndef COMPAT53_HAVE_STRERROR_R 31 | # if (!defined(_WIN32)) && \ 32 | ((defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 200112L) || \ 33 | (defined(_XOPEN_SOURCE) && _XOPEN_SOURCE >= 600) || \ 34 | defined(__APPLE__)) 35 | # define COMPAT53_HAVE_STRERROR_R 1 36 | # else /* none of the defines matched: define to 0 */ 37 | # define COMPAT53_HAVE_STRERROR_R 0 38 | # endif /* have strerror_r of some form */ 39 | #endif /* strerror_r */ 40 | 41 | #ifndef COMPAT53_HAVE_STRERROR_S 42 | # if defined(_MSC_VER) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L && \ 43 | defined(__STDC_LIB_EXT1__) && __STDC_LIB_EXT1__) 44 | # define COMPAT53_HAVE_STRERROR_S 1 45 | # else /* not VC++ or C11 */ 46 | # define COMPAT53_HAVE_STRERROR_S 0 47 | # endif /* strerror_s from VC++ or C11 */ 48 | #endif /* strerror_s */ 49 | 50 | #ifndef COMPAT53_LUA_FILE_BUFFER_SIZE 51 | # define COMPAT53_LUA_FILE_BUFFER_SIZE 4096 52 | #endif /* Lua File Buffer Size */ 53 | 54 | 55 | static char* compat53_strerror (int en, char* buff, size_t sz) { 56 | #if COMPAT53_HAVE_STRERROR_R 57 | /* use strerror_r here, because it's available on these specific platforms */ 58 | if (sz > 0) { 59 | buff[0] = '\0'; 60 | /* we don't care whether the GNU version or the XSI version is used: */ 61 | if (strerror_r(en, buff, sz)) { 62 | /* Yes, we really DO want to ignore the return value! 63 | * GCC makes that extra hard, not even a (void) cast will do. */ 64 | } 65 | if (buff[0] == '\0') { 66 | /* Buffer is unchanged, so we probably have called GNU strerror_r which 67 | * returned a static constant string. Chances are that strerror will 68 | * return the same static constant string and therefore be thread-safe. */ 69 | return strerror(en); 70 | } 71 | } 72 | return buff; /* sz is 0 *or* strerror_r wrote into the buffer */ 73 | #elif COMPAT53_HAVE_STRERROR_S 74 | /* for MSVC and other C11 implementations, use strerror_s since it's 75 | * provided by default by the libraries */ 76 | strerror_s(buff, sz, en); 77 | return buff; 78 | #else 79 | /* fallback, but strerror is not guaranteed to be threadsafe due to modifying 80 | * errno itself and some impls not locking a static buffer for it ... but most 81 | * known systems have threadsafe errno: this might only change if the locale 82 | * is changed out from under someone while this function is being called */ 83 | (void)buff; 84 | (void)sz; 85 | return strerror(en); 86 | #endif 87 | } 88 | 89 | 90 | COMPAT53_API int lua_absindex (lua_State *L, int i) { 91 | if (i < 0 && i > LUA_REGISTRYINDEX) 92 | i += lua_gettop(L) + 1; 93 | return i; 94 | } 95 | 96 | 97 | static void compat53_call_lua (lua_State *L, char const code[], size_t len, 98 | int nargs, int nret) { 99 | lua_rawgetp(L, LUA_REGISTRYINDEX, (void*)code); 100 | if (lua_type(L, -1) != LUA_TFUNCTION) { 101 | lua_pop(L, 1); 102 | if (luaL_loadbuffer(L, code, len, "=none")) 103 | lua_error(L); 104 | lua_pushvalue(L, -1); 105 | lua_rawsetp(L, LUA_REGISTRYINDEX, (void*)code); 106 | } 107 | lua_insert(L, -nargs-1); 108 | lua_call(L, nargs, nret); 109 | } 110 | 111 | 112 | static const char compat53_arith_code[] = 113 | "local op,a,b=...\n" 114 | "if op==0 then return a+b\n" 115 | "elseif op==1 then return a-b\n" 116 | "elseif op==2 then return a*b\n" 117 | "elseif op==3 then return a/b\n" 118 | "elseif op==4 then return a%b\n" 119 | "elseif op==5 then return a^b\n" 120 | "elseif op==6 then return -a\n" 121 | "end\n"; 122 | 123 | COMPAT53_API void lua_arith (lua_State *L, int op) { 124 | if (op < LUA_OPADD || op > LUA_OPUNM) 125 | luaL_error(L, "invalid 'op' argument for lua_arith"); 126 | luaL_checkstack(L, 5, "not enough stack slots"); 127 | if (op == LUA_OPUNM) 128 | lua_pushvalue(L, -1); 129 | lua_pushnumber(L, op); 130 | lua_insert(L, -3); 131 | compat53_call_lua(L, compat53_arith_code, 132 | sizeof(compat53_arith_code)-1, 3, 1); 133 | } 134 | 135 | 136 | static const char compat53_compare_code[] = 137 | "local a,b=...\n" 138 | "return a<=b\n"; 139 | 140 | COMPAT53_API int lua_compare (lua_State *L, int idx1, int idx2, int op) { 141 | int result = 0; 142 | switch (op) { 143 | case LUA_OPEQ: 144 | return lua_equal(L, idx1, idx2); 145 | case LUA_OPLT: 146 | return lua_lessthan(L, idx1, idx2); 147 | case LUA_OPLE: 148 | luaL_checkstack(L, 5, "not enough stack slots"); 149 | idx1 = lua_absindex(L, idx1); 150 | idx2 = lua_absindex(L, idx2); 151 | lua_pushvalue(L, idx1); 152 | lua_pushvalue(L, idx2); 153 | compat53_call_lua(L, compat53_compare_code, 154 | sizeof(compat53_compare_code)-1, 2, 1); 155 | result = lua_toboolean(L, -1); 156 | lua_pop(L, 1); 157 | return result; 158 | default: 159 | luaL_error(L, "invalid 'op' argument for lua_compare"); 160 | } 161 | return 0; 162 | } 163 | 164 | 165 | COMPAT53_API void lua_copy (lua_State *L, int from, int to) { 166 | int abs_to = lua_absindex(L, to); 167 | luaL_checkstack(L, 1, "not enough stack slots"); 168 | lua_pushvalue(L, from); 169 | lua_replace(L, abs_to); 170 | } 171 | 172 | 173 | COMPAT53_API void lua_len (lua_State *L, int i) { 174 | switch (lua_type(L, i)) { 175 | case LUA_TSTRING: 176 | lua_pushnumber(L, (lua_Number)lua_objlen(L, i)); 177 | break; 178 | case LUA_TTABLE: 179 | if (!luaL_callmeta(L, i, "__len")) 180 | lua_pushnumber(L, (lua_Number)lua_objlen(L, i)); 181 | break; 182 | case LUA_TUSERDATA: 183 | if (luaL_callmeta(L, i, "__len")) 184 | break; 185 | /* FALLTHROUGH */ 186 | default: 187 | luaL_error(L, "attempt to get length of a %s value", 188 | lua_typename(L, lua_type(L, i))); 189 | } 190 | } 191 | 192 | 193 | COMPAT53_API int lua_rawgetp (lua_State *L, int i, const void *p) { 194 | int abs_i = lua_absindex(L, i); 195 | lua_pushlightuserdata(L, (void*)p); 196 | lua_rawget(L, abs_i); 197 | return lua_type(L, -1); 198 | } 199 | 200 | COMPAT53_API void lua_rawsetp (lua_State *L, int i, const void *p) { 201 | int abs_i = lua_absindex(L, i); 202 | luaL_checkstack(L, 1, "not enough stack slots"); 203 | lua_pushlightuserdata(L, (void*)p); 204 | lua_insert(L, -2); 205 | lua_rawset(L, abs_i); 206 | } 207 | 208 | 209 | COMPAT53_API lua_Number lua_tonumberx (lua_State *L, int i, int *isnum) { 210 | lua_Number n = lua_tonumber(L, i); 211 | if (isnum != NULL) { 212 | *isnum = (n != 0 || lua_isnumber(L, i)); 213 | } 214 | return n; 215 | } 216 | 217 | 218 | COMPAT53_API void luaL_checkversion (lua_State *L) { 219 | (void)L; 220 | } 221 | 222 | 223 | COMPAT53_API void luaL_checkstack (lua_State *L, int sp, const char *msg) { 224 | if (!lua_checkstack(L, sp+LUA_MINSTACK)) { 225 | if (msg != NULL) 226 | luaL_error(L, "stack overflow (%s)", msg); 227 | else { 228 | lua_pushliteral(L, "stack overflow"); 229 | lua_error(L); 230 | } 231 | } 232 | } 233 | 234 | 235 | COMPAT53_API int luaL_getsubtable (lua_State *L, int i, const char *name) { 236 | int abs_i = lua_absindex(L, i); 237 | luaL_checkstack(L, 3, "not enough stack slots"); 238 | lua_pushstring(L, name); 239 | lua_gettable(L, abs_i); 240 | if (lua_istable(L, -1)) 241 | return 1; 242 | lua_pop(L, 1); 243 | lua_newtable(L); 244 | lua_pushstring(L, name); 245 | lua_pushvalue(L, -2); 246 | lua_settable(L, abs_i); 247 | return 0; 248 | } 249 | 250 | 251 | COMPAT53_API lua_Integer luaL_len (lua_State *L, int i) { 252 | lua_Integer res = 0; 253 | int isnum = 0; 254 | luaL_checkstack(L, 1, "not enough stack slots"); 255 | lua_len(L, i); 256 | res = lua_tointegerx(L, -1, &isnum); 257 | lua_pop(L, 1); 258 | if (!isnum) 259 | luaL_error(L, "object length is not an integer"); 260 | return res; 261 | } 262 | 263 | 264 | COMPAT53_API void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) { 265 | luaL_checkstack(L, nup+1, "too many upvalues"); 266 | for (; l->name != NULL; l++) { /* fill the table with given functions */ 267 | int i; 268 | lua_pushstring(L, l->name); 269 | for (i = 0; i < nup; i++) /* copy upvalues to the top */ 270 | lua_pushvalue(L, -(nup + 1)); 271 | lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ 272 | lua_settable(L, -(nup + 3)); /* table must be below the upvalues, the name and the closure */ 273 | } 274 | lua_pop(L, nup); /* remove upvalues */ 275 | } 276 | 277 | 278 | COMPAT53_API void luaL_setmetatable (lua_State *L, const char *tname) { 279 | luaL_checkstack(L, 1, "not enough stack slots"); 280 | luaL_getmetatable(L, tname); 281 | lua_setmetatable(L, -2); 282 | } 283 | 284 | 285 | COMPAT53_API void *luaL_testudata (lua_State *L, int i, const char *tname) { 286 | void *p = lua_touserdata(L, i); 287 | luaL_checkstack(L, 2, "not enough stack slots"); 288 | if (p == NULL || !lua_getmetatable(L, i)) 289 | return NULL; 290 | else { 291 | int res = 0; 292 | luaL_getmetatable(L, tname); 293 | res = lua_rawequal(L, -1, -2); 294 | lua_pop(L, 2); 295 | if (!res) 296 | p = NULL; 297 | } 298 | return p; 299 | } 300 | 301 | 302 | static int compat53_countlevels (lua_State *L) { 303 | lua_Debug ar; 304 | int li = 1, le = 1; 305 | /* find an upper bound */ 306 | while (lua_getstack(L, le, &ar)) { li = le; le *= 2; } 307 | /* do a binary search */ 308 | while (li < le) { 309 | int m = (li + le)/2; 310 | if (lua_getstack(L, m, &ar)) li = m + 1; 311 | else le = m; 312 | } 313 | return le - 1; 314 | } 315 | 316 | static int compat53_findfield (lua_State *L, int objidx, int level) { 317 | if (level == 0 || !lua_istable(L, -1)) 318 | return 0; /* not found */ 319 | lua_pushnil(L); /* start 'next' loop */ 320 | while (lua_next(L, -2)) { /* for each pair in table */ 321 | if (lua_type(L, -2) == LUA_TSTRING) { /* ignore non-string keys */ 322 | if (lua_rawequal(L, objidx, -1)) { /* found object? */ 323 | lua_pop(L, 1); /* remove value (but keep name) */ 324 | return 1; 325 | } 326 | else if (compat53_findfield(L, objidx, level - 1)) { /* try recursively */ 327 | lua_remove(L, -2); /* remove table (but keep name) */ 328 | lua_pushliteral(L, "."); 329 | lua_insert(L, -2); /* place '.' between the two names */ 330 | lua_concat(L, 3); 331 | return 1; 332 | } 333 | } 334 | lua_pop(L, 1); /* remove value */ 335 | } 336 | return 0; /* not found */ 337 | } 338 | 339 | static int compat53_pushglobalfuncname (lua_State *L, lua_Debug *ar) { 340 | int top = lua_gettop(L); 341 | lua_getinfo(L, "f", ar); /* push function */ 342 | lua_pushvalue(L, LUA_GLOBALSINDEX); 343 | if (compat53_findfield(L, top + 1, 2)) { 344 | lua_copy(L, -1, top + 1); /* move name to proper place */ 345 | lua_pop(L, 2); /* remove pushed values */ 346 | return 1; 347 | } 348 | else { 349 | lua_settop(L, top); /* remove function and global table */ 350 | return 0; 351 | } 352 | } 353 | 354 | static void compat53_pushfuncname (lua_State *L, lua_Debug *ar) { 355 | if (*ar->namewhat != '\0') /* is there a name? */ 356 | lua_pushfstring(L, "function " LUA_QS, ar->name); 357 | else if (*ar->what == 'm') /* main? */ 358 | lua_pushliteral(L, "main chunk"); 359 | else if (*ar->what == 'C') { 360 | if (compat53_pushglobalfuncname(L, ar)) { 361 | lua_pushfstring(L, "function " LUA_QS, lua_tostring(L, -1)); 362 | lua_remove(L, -2); /* remove name */ 363 | } 364 | else 365 | lua_pushliteral(L, "?"); 366 | } 367 | else 368 | lua_pushfstring(L, "function <%s:%d>", ar->short_src, ar->linedefined); 369 | } 370 | 371 | #define COMPAT53_LEVELS1 12 /* size of the first part of the stack */ 372 | #define COMPAT53_LEVELS2 10 /* size of the second part of the stack */ 373 | 374 | COMPAT53_API void luaL_traceback (lua_State *L, lua_State *L1, 375 | const char *msg, int level) { 376 | lua_Debug ar; 377 | int top = lua_gettop(L); 378 | int numlevels = compat53_countlevels(L1); 379 | int mark = (numlevels > COMPAT53_LEVELS1 + COMPAT53_LEVELS2) ? COMPAT53_LEVELS1 : 0; 380 | if (msg) lua_pushfstring(L, "%s\n", msg); 381 | lua_pushliteral(L, "stack traceback:"); 382 | while (lua_getstack(L1, level++, &ar)) { 383 | if (level == mark) { /* too many levels? */ 384 | lua_pushliteral(L, "\n\t..."); /* add a '...' */ 385 | level = numlevels - COMPAT53_LEVELS2; /* and skip to last ones */ 386 | } 387 | else { 388 | lua_getinfo(L1, "Slnt", &ar); 389 | lua_pushfstring(L, "\n\t%s:", ar.short_src); 390 | if (ar.currentline > 0) 391 | lua_pushfstring(L, "%d:", ar.currentline); 392 | lua_pushliteral(L, " in "); 393 | compat53_pushfuncname(L, &ar); 394 | lua_concat(L, lua_gettop(L) - top); 395 | } 396 | } 397 | lua_concat(L, lua_gettop(L) - top); 398 | } 399 | 400 | 401 | COMPAT53_API int luaL_fileresult (lua_State *L, int stat, const char *fname) { 402 | const char *serr = NULL; 403 | int en = errno; /* calls to Lua API may change this value */ 404 | char buf[512] = { 0 }; 405 | if (stat) { 406 | lua_pushboolean(L, 1); 407 | return 1; 408 | } 409 | else { 410 | lua_pushnil(L); 411 | serr = compat53_strerror(en, buf, sizeof(buf)); 412 | if (fname) 413 | lua_pushfstring(L, "%s: %s", fname, serr); 414 | else 415 | lua_pushstring(L, serr); 416 | lua_pushnumber(L, (lua_Number)en); 417 | return 3; 418 | } 419 | } 420 | 421 | 422 | static int compat53_checkmode (lua_State *L, const char *mode, const char *modename, int err) { 423 | if (mode && strchr(mode, modename[0]) == NULL) { 424 | lua_pushfstring(L, "attempt to load a %s chunk (mode is '%s')", modename, mode); 425 | return err; 426 | } 427 | return LUA_OK; 428 | } 429 | 430 | 431 | typedef struct { 432 | lua_Reader reader; 433 | void *ud; 434 | int has_peeked_data; 435 | const char *peeked_data; 436 | size_t peeked_data_size; 437 | } compat53_reader_data; 438 | 439 | 440 | static const char *compat53_reader (lua_State *L, void *ud, size_t *size) { 441 | compat53_reader_data *data = (compat53_reader_data *)ud; 442 | if (data->has_peeked_data) { 443 | data->has_peeked_data = 0; 444 | *size = data->peeked_data_size; 445 | return data->peeked_data; 446 | } else 447 | return data->reader(L, data->ud, size); 448 | } 449 | 450 | 451 | COMPAT53_API int lua_load (lua_State *L, lua_Reader reader, void *data, const char *source, const char *mode) { 452 | int status = LUA_OK; 453 | compat53_reader_data compat53_data = { 0, NULL, 1, 0, 0 }; 454 | compat53_data.reader = reader; 455 | compat53_data.ud = data; 456 | compat53_data.peeked_data = reader(L, data, &(compat53_data.peeked_data_size)); 457 | if (compat53_data.peeked_data && compat53_data.peeked_data_size && 458 | compat53_data.peeked_data[0] == LUA_SIGNATURE[0]) /* binary file? */ 459 | status = compat53_checkmode(L, mode, "binary", LUA_ERRSYNTAX); 460 | else 461 | status = compat53_checkmode(L, mode, "text", LUA_ERRSYNTAX); 462 | if (status != LUA_OK) 463 | return status; 464 | /* we need to call the original 5.1 version of lua_load! */ 465 | #undef lua_load 466 | return lua_load(L, compat53_reader, &compat53_data, source); 467 | #define lua_load COMPAT53_CONCAT(COMPAT53_PREFIX, _load_53) 468 | } 469 | 470 | 471 | typedef struct { 472 | int n; /* number of pre-read characters */ 473 | FILE *f; /* file being read */ 474 | char buff[COMPAT53_LUA_FILE_BUFFER_SIZE]; /* area for reading file */ 475 | } compat53_LoadF; 476 | 477 | 478 | static const char *compat53_getF (lua_State *L, void *ud, size_t *size) { 479 | compat53_LoadF *lf = (compat53_LoadF *)ud; 480 | (void)L; /* not used */ 481 | if (lf->n > 0) { /* are there pre-read characters to be read? */ 482 | *size = lf->n; /* return them (chars already in buffer) */ 483 | lf->n = 0; /* no more pre-read characters */ 484 | } 485 | else { /* read a block from file */ 486 | /* 'fread' can return > 0 *and* set the EOF flag. If next call to 487 | 'compat53_getF' called 'fread', it might still wait for user input. 488 | The next check avoids this problem. */ 489 | if (feof(lf->f)) return NULL; 490 | *size = fread(lf->buff, 1, sizeof(lf->buff), lf->f); /* read block */ 491 | } 492 | return lf->buff; 493 | } 494 | 495 | 496 | static int compat53_errfile (lua_State *L, const char *what, int fnameindex) { 497 | char buf[512] = {0}; 498 | const char *serr = compat53_strerror(errno, buf, sizeof(buf)); 499 | const char *filename = lua_tostring(L, fnameindex) + 1; 500 | lua_pushfstring(L, "cannot %s %s: %s", what, filename, serr); 501 | lua_remove(L, fnameindex); 502 | return LUA_ERRFILE; 503 | } 504 | 505 | 506 | static int compat53_skipBOM (compat53_LoadF *lf) { 507 | const char *p = "\xEF\xBB\xBF"; /* UTF-8 BOM mark */ 508 | int c; 509 | lf->n = 0; 510 | do { 511 | c = getc(lf->f); 512 | if (c == EOF || c != *(const unsigned char *)p++) return c; 513 | lf->buff[lf->n++] = (char)c; /* to be read by the parser */ 514 | } while (*p != '\0'); 515 | lf->n = 0; /* prefix matched; discard it */ 516 | return getc(lf->f); /* return next character */ 517 | } 518 | 519 | 520 | /* 521 | ** reads the first character of file 'f' and skips an optional BOM mark 522 | ** in its beginning plus its first line if it starts with '#'. Returns 523 | ** true if it skipped the first line. In any case, '*cp' has the 524 | ** first "valid" character of the file (after the optional BOM and 525 | ** a first-line comment). 526 | */ 527 | static int compat53_skipcomment (compat53_LoadF *lf, int *cp) { 528 | int c = *cp = compat53_skipBOM(lf); 529 | if (c == '#') { /* first line is a comment (Unix exec. file)? */ 530 | do { /* skip first line */ 531 | c = getc(lf->f); 532 | } while (c != EOF && c != '\n'); 533 | *cp = getc(lf->f); /* skip end-of-line, if present */ 534 | return 1; /* there was a comment */ 535 | } 536 | else return 0; /* no comment */ 537 | } 538 | 539 | 540 | COMPAT53_API int luaL_loadfilex (lua_State *L, const char *filename, const char *mode) { 541 | compat53_LoadF lf; 542 | int status, readstatus; 543 | int c; 544 | int fnameindex = lua_gettop(L) + 1; /* index of filename on the stack */ 545 | if (filename == NULL) { 546 | lua_pushliteral(L, "=stdin"); 547 | lf.f = stdin; 548 | } 549 | else { 550 | lua_pushfstring(L, "@%s", filename); 551 | #if defined(_MSC_VER) 552 | /* This code is here to stop a deprecation error that stops builds 553 | * if a certain macro is defined. While normally not caring would 554 | * be best, some header-only libraries and builds can't afford to 555 | * dictate this to the user. A quick check shows that fopen_s this 556 | * goes back to VS 2005, and _fsopen goes back to VS 2003 .NET, 557 | * possibly even before that so we don't need to do any version 558 | * number checks, since this has been there since forever. */ 559 | 560 | /* TO USER: if you want the behavior of typical fopen_s/fopen, 561 | * which does lock the file on VC++, define the macro used below to 0 */ 562 | #if COMPAT53_FOPEN_NO_LOCK 563 | lf.f = _fsopen(filename, "r", _SH_DENYNO); /* do not lock the file in any way */ 564 | if (lf.f == NULL) 565 | return compat53_errfile(L, "open", fnameindex); 566 | #else /* use default locking version */ 567 | if (fopen_s(&lf.f, filename, "r") != 0) 568 | return compat53_errfile(L, "open", fnameindex); 569 | #endif /* Locking vs. No-locking fopen variants */ 570 | #else 571 | lf.f = fopen(filename, "r"); /* default stdlib doesn't forcefully lock files here */ 572 | if (lf.f == NULL) return compat53_errfile(L, "open", fnameindex); 573 | #endif 574 | } 575 | if (compat53_skipcomment(&lf, &c)) /* read initial portion */ 576 | lf.buff[lf.n++] = '\n'; /* add line to correct line numbers */ 577 | if (c == LUA_SIGNATURE[0] && filename) { /* binary file? */ 578 | #if defined(_MSC_VER) 579 | if (freopen_s(&lf.f, filename, "rb", lf.f) != 0) 580 | return compat53_errfile(L, "reopen", fnameindex); 581 | #else 582 | lf.f = freopen(filename, "rb", lf.f); /* reopen in binary mode */ 583 | if (lf.f == NULL) return compat53_errfile(L, "reopen", fnameindex); 584 | #endif 585 | compat53_skipcomment(&lf, &c); /* re-read initial portion */ 586 | } 587 | if (c != EOF) 588 | lf.buff[lf.n++] = (char)c; /* 'c' is the first character of the stream */ 589 | status = lua_load(L, &compat53_getF, &lf, lua_tostring(L, -1), mode); 590 | readstatus = ferror(lf.f); 591 | if (filename) fclose(lf.f); /* close file (even in case of errors) */ 592 | if (readstatus) { 593 | lua_settop(L, fnameindex); /* ignore results from 'lua_load' */ 594 | return compat53_errfile(L, "read", fnameindex); 595 | } 596 | lua_remove(L, fnameindex); 597 | return status; 598 | } 599 | 600 | 601 | COMPAT53_API int luaL_loadbufferx (lua_State *L, const char *buff, size_t sz, const char *name, const char *mode) { 602 | int status = LUA_OK; 603 | if (sz > 0 && buff[0] == LUA_SIGNATURE[0]) { 604 | status = compat53_checkmode(L, mode, "binary", LUA_ERRSYNTAX); 605 | } 606 | else { 607 | status = compat53_checkmode(L, mode, "text", LUA_ERRSYNTAX); 608 | } 609 | if (status != LUA_OK) 610 | return status; 611 | return luaL_loadbuffer(L, buff, sz, name); 612 | } 613 | 614 | 615 | #if !defined(l_inspectstat) && \ 616 | (defined(unix) || defined(__unix) || defined(__unix__) || \ 617 | defined(__TOS_AIX__) || defined(_SYSTYPE_BSD) || \ 618 | (defined(__APPLE__) && defined(__MACH__))) 619 | /* some form of unix; check feature macros in unistd.h for details */ 620 | # include 621 | /* check posix version; the relevant include files and macros probably 622 | * were available before 2001, but I'm not sure */ 623 | # if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L 624 | # include 625 | # define l_inspectstat(stat,what) \ 626 | if (WIFEXITED(stat)) { stat = WEXITSTATUS(stat); } \ 627 | else if (WIFSIGNALED(stat)) { stat = WTERMSIG(stat); what = "signal"; } 628 | # endif 629 | #endif 630 | 631 | /* provide default (no-op) version */ 632 | #if !defined(l_inspectstat) 633 | # define l_inspectstat(stat,what) ((void)0) 634 | #endif 635 | 636 | 637 | COMPAT53_API int luaL_execresult (lua_State *L, int stat) { 638 | const char *what = "exit"; 639 | if (stat == -1) 640 | return luaL_fileresult(L, 0, NULL); 641 | else { 642 | l_inspectstat(stat, what); 643 | if (*what == 'e' && stat == 0) 644 | lua_pushboolean(L, 1); 645 | else 646 | lua_pushnil(L); 647 | lua_pushstring(L, what); 648 | lua_pushinteger(L, stat); 649 | return 3; 650 | } 651 | } 652 | 653 | 654 | COMPAT53_API void luaL_buffinit (lua_State *L, luaL_Buffer_53 *B) { 655 | /* make it crash if used via pointer to a 5.1-style luaL_Buffer */ 656 | B->b.p = NULL; 657 | B->b.L = NULL; 658 | B->b.lvl = 0; 659 | /* reuse the buffer from the 5.1-style luaL_Buffer though! */ 660 | B->ptr = B->b.buffer; 661 | B->capacity = LUAL_BUFFERSIZE; 662 | B->nelems = 0; 663 | B->L2 = L; 664 | } 665 | 666 | 667 | COMPAT53_API char *luaL_prepbuffsize (luaL_Buffer_53 *B, size_t s) { 668 | if (B->capacity - B->nelems < s) { /* needs to grow */ 669 | char* newptr = NULL; 670 | size_t newcap = B->capacity * 2; 671 | if (newcap - B->nelems < s) 672 | newcap = B->nelems + s; 673 | if (newcap < B->capacity) /* overflow */ 674 | luaL_error(B->L2, "buffer too large"); 675 | newptr = (char*)lua_newuserdata(B->L2, newcap); 676 | memcpy(newptr, B->ptr, B->nelems); 677 | if (B->ptr != B->b.buffer) 678 | lua_replace(B->L2, -2); /* remove old buffer */ 679 | B->ptr = newptr; 680 | B->capacity = newcap; 681 | } 682 | return B->ptr+B->nelems; 683 | } 684 | 685 | 686 | COMPAT53_API void luaL_addlstring (luaL_Buffer_53 *B, const char *s, size_t l) { 687 | memcpy(luaL_prepbuffsize(B, l), s, l); 688 | luaL_addsize(B, l); 689 | } 690 | 691 | 692 | COMPAT53_API void luaL_addvalue (luaL_Buffer_53 *B) { 693 | size_t len = 0; 694 | const char *s = lua_tolstring(B->L2, -1, &len); 695 | if (!s) 696 | luaL_error(B->L2, "cannot convert value to string"); 697 | if (B->ptr != B->b.buffer) 698 | lua_insert(B->L2, -2); /* userdata buffer must be at stack top */ 699 | luaL_addlstring(B, s, len); 700 | lua_remove(B->L2, B->ptr != B->b.buffer ? -2 : -1); 701 | } 702 | 703 | 704 | void luaL_pushresult (luaL_Buffer_53 *B) { 705 | lua_pushlstring(B->L2, B->ptr, B->nelems); 706 | if (B->ptr != B->b.buffer) 707 | lua_replace(B->L2, -2); /* remove userdata buffer */ 708 | } 709 | 710 | 711 | #endif /* Lua 5.1 */ 712 | 713 | 714 | 715 | /* definitions for Lua 5.1 and Lua 5.2 */ 716 | #if defined( LUA_VERSION_NUM ) && LUA_VERSION_NUM <= 502 717 | 718 | 719 | COMPAT53_API const char *lua_pushlstring (lua_State *L, const char *s, size_t len) { 720 | #undef lua_pushlstring 721 | lua_pushlstring(L, len > 0 ? s : "", len); 722 | #define lua_pushlstring COMPAT53_CONCAT(COMPAT53_PREFIX, _pushlstring_53) 723 | return lua_tostring(L, -1); 724 | } 725 | 726 | 727 | COMPAT53_API int lua_geti (lua_State *L, int index, lua_Integer i) { 728 | index = lua_absindex(L, index); 729 | lua_pushinteger(L, i); 730 | lua_gettable(L, index); 731 | return lua_type(L, -1); 732 | } 733 | 734 | 735 | #ifndef LUA_EXTRASPACE 736 | #define LUA_EXTRASPACE (sizeof(void*)) 737 | #endif 738 | 739 | COMPAT53_API void *lua_getextraspace (lua_State *L) { 740 | int is_main = 0; 741 | void *ptr = NULL; 742 | luaL_checkstack(L, 4, "not enough stack slots available"); 743 | lua_pushliteral(L, "__compat53_extraspace"); 744 | lua_pushvalue(L, -1); 745 | lua_rawget(L, LUA_REGISTRYINDEX); 746 | if (!lua_istable(L, -1)) { 747 | lua_pop(L, 1); 748 | lua_createtable(L, 0, 2); 749 | lua_createtable(L, 0, 1); 750 | lua_pushliteral(L, "k"); 751 | lua_setfield(L, -2, "__mode"); 752 | lua_setmetatable(L, -2); 753 | lua_pushvalue(L, -2); 754 | lua_pushvalue(L, -2); 755 | lua_rawset(L, LUA_REGISTRYINDEX); 756 | } 757 | lua_replace(L, -2); 758 | is_main = lua_pushthread(L); 759 | lua_rawget(L, -2); 760 | ptr = lua_touserdata(L, -1); 761 | if (!ptr) { 762 | lua_pop(L, 1); 763 | ptr = lua_newuserdata(L, LUA_EXTRASPACE); 764 | if (is_main) { 765 | memset(ptr, '\0', LUA_EXTRASPACE); 766 | lua_pushthread(L); 767 | lua_pushvalue(L, -2); 768 | lua_rawset(L, -4); 769 | lua_pushboolean(L, 1); 770 | lua_pushvalue(L, -2); 771 | lua_rawset(L, -4); 772 | } else { 773 | void* mptr = NULL; 774 | lua_pushboolean(L, 1); 775 | lua_rawget(L, -3); 776 | mptr = lua_touserdata(L, -1); 777 | if (mptr) 778 | memcpy(ptr, mptr, LUA_EXTRASPACE); 779 | else 780 | memset(ptr, '\0', LUA_EXTRASPACE); 781 | lua_pop(L, 1); 782 | lua_pushthread(L); 783 | lua_pushvalue(L, -2); 784 | lua_rawset(L, -4); 785 | } 786 | } 787 | lua_pop(L, 2); 788 | return ptr; 789 | } 790 | 791 | 792 | COMPAT53_API int lua_isinteger (lua_State *L, int index) { 793 | if (lua_type(L, index) == LUA_TNUMBER) { 794 | lua_Number n = lua_tonumber(L, index); 795 | lua_Integer i = lua_tointeger(L, index); 796 | if (i == n) 797 | return 1; 798 | } 799 | return 0; 800 | } 801 | 802 | 803 | COMPAT53_API lua_Integer lua_tointegerx (lua_State *L, int i, int *isnum) { 804 | int ok = 0; 805 | lua_Number n = lua_tonumberx(L, i, &ok); 806 | if (ok) { 807 | if (n == (lua_Integer)n) { 808 | if (isnum) 809 | *isnum = 1; 810 | return (lua_Integer)n; 811 | } 812 | } 813 | if (isnum) 814 | *isnum = 0; 815 | return 0; 816 | } 817 | 818 | 819 | static void compat53_reverse (lua_State *L, int a, int b) { 820 | for (; a < b; ++a, --b) { 821 | lua_pushvalue(L, a); 822 | lua_pushvalue(L, b); 823 | lua_replace(L, a); 824 | lua_replace(L, b); 825 | } 826 | } 827 | 828 | 829 | COMPAT53_API void lua_rotate (lua_State *L, int idx, int n) { 830 | int n_elems = 0; 831 | idx = lua_absindex(L, idx); 832 | n_elems = lua_gettop(L)-idx+1; 833 | if (n < 0) 834 | n += n_elems; 835 | if ( n > 0 && n < n_elems) { 836 | luaL_checkstack(L, 2, "not enough stack slots available"); 837 | n = n_elems - n; 838 | compat53_reverse(L, idx, idx+n-1); 839 | compat53_reverse(L, idx+n, idx+n_elems-1); 840 | compat53_reverse(L, idx, idx+n_elems-1); 841 | } 842 | } 843 | 844 | 845 | COMPAT53_API void lua_seti (lua_State *L, int index, lua_Integer i) { 846 | luaL_checkstack(L, 1, "not enough stack slots available"); 847 | index = lua_absindex(L, index); 848 | lua_pushinteger(L, i); 849 | lua_insert(L, -2); 850 | lua_settable(L, index); 851 | } 852 | 853 | 854 | #if !defined(lua_str2number) 855 | # define lua_str2number(s, p) strtod((s), (p)) 856 | #endif 857 | 858 | COMPAT53_API size_t lua_stringtonumber (lua_State *L, const char *s) { 859 | char* endptr; 860 | lua_Number n = lua_str2number(s, &endptr); 861 | if (endptr != s) { 862 | while (*endptr != '\0' && isspace((unsigned char)*endptr)) 863 | ++endptr; 864 | if (*endptr == '\0') { 865 | lua_pushnumber(L, n); 866 | return endptr - s + 1; 867 | } 868 | } 869 | return 0; 870 | } 871 | 872 | 873 | COMPAT53_API const char *luaL_tolstring (lua_State *L, int idx, size_t *len) { 874 | if (!luaL_callmeta(L, idx, "__tostring")) { 875 | int t = lua_type(L, idx), tt = 0; 876 | char const* name = NULL; 877 | switch (t) { 878 | case LUA_TNIL: 879 | lua_pushliteral(L, "nil"); 880 | break; 881 | case LUA_TSTRING: 882 | case LUA_TNUMBER: 883 | lua_pushvalue(L, idx); 884 | break; 885 | case LUA_TBOOLEAN: 886 | if (lua_toboolean(L, idx)) 887 | lua_pushliteral(L, "true"); 888 | else 889 | lua_pushliteral(L, "false"); 890 | break; 891 | default: 892 | tt = luaL_getmetafield(L, idx, "__name"); 893 | name = (tt == LUA_TSTRING) ? lua_tostring(L, -1) : lua_typename(L, t); 894 | lua_pushfstring(L, "%s: %p", name, lua_topointer(L, idx)); 895 | if (tt != LUA_TNIL) 896 | lua_replace(L, -2); 897 | break; 898 | } 899 | } else { 900 | if (!lua_isstring(L, -1)) 901 | luaL_error(L, "'__tostring' must return a string"); 902 | } 903 | return lua_tolstring(L, -1, len); 904 | } 905 | 906 | 907 | COMPAT53_API void luaL_requiref (lua_State *L, const char *modname, 908 | lua_CFunction openf, int glb) { 909 | luaL_checkstack(L, 3, "not enough stack slots available"); 910 | luaL_getsubtable(L, LUA_REGISTRYINDEX, "_LOADED"); 911 | if (lua_getfield(L, -1, modname) == LUA_TNIL) { 912 | lua_pop(L, 1); 913 | lua_pushcfunction(L, openf); 914 | lua_pushstring(L, modname); 915 | lua_call(L, 1, 1); 916 | lua_pushvalue(L, -1); 917 | lua_setfield(L, -3, modname); 918 | } 919 | if (glb) { 920 | lua_pushvalue(L, -1); 921 | lua_setglobal(L, modname); 922 | } 923 | lua_replace(L, -2); 924 | } 925 | 926 | 927 | #endif /* Lua 5.1 and 5.2 */ 928 | 929 | 930 | #endif /* COMPAT53_C_ */ 931 | 932 | 933 | /********************************************************************* 934 | * This file contains parts of Lua 5.2's and Lua 5.3's source code: 935 | * 936 | * Copyright (C) 1994-2014 Lua.org, PUC-Rio. 937 | * 938 | * Permission is hereby granted, free of charge, to any person obtaining 939 | * a copy of this software and associated documentation files (the 940 | * "Software"), to deal in the Software without restriction, including 941 | * without limitation the rights to use, copy, modify, merge, publish, 942 | * distribute, sublicense, and/or sell copies of the Software, and to 943 | * permit persons to whom the Software is furnished to do so, subject to 944 | * the following conditions: 945 | * 946 | * The above copyright notice and this permission notice shall be 947 | * included in all copies or substantial portions of the Software. 948 | * 949 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 950 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 951 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 952 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 953 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 954 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 955 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 956 | *********************************************************************/ 957 | 958 | -------------------------------------------------------------------------------- /lib/compat-5.3/compat-5.3.h: -------------------------------------------------------------------------------- 1 | #ifndef COMPAT53_H_ 2 | #define COMPAT53_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #if defined(__cplusplus) && !defined(COMPAT53_LUA_CPP) 8 | extern "C" { 9 | #endif 10 | #include 11 | #include 12 | #include 13 | #if defined(__cplusplus) && !defined(COMPAT53_LUA_CPP) 14 | } 15 | #endif 16 | 17 | 18 | #undef COMPAT53_INCLUDE_SOURCE 19 | #if defined(COMPAT53_PREFIX) 20 | /* - change the symbol names of functions to avoid linker conflicts 21 | * - compat-5.3.c needs to be compiled (and linked) separately 22 | */ 23 | # if !defined(COMPAT53_API) 24 | # define COMPAT53_API extern 25 | # endif 26 | #else /* COMPAT53_PREFIX */ 27 | /* - make all functions static and include the source. 28 | * - compat-5.3.c doesn't need to be compiled (and linked) separately 29 | */ 30 | # define COMPAT53_PREFIX compat53 31 | # undef COMPAT53_API 32 | # if defined(__GNUC__) || defined(__clang__) 33 | # define COMPAT53_API __attribute__((__unused__)) static 34 | # else 35 | # define COMPAT53_API static 36 | # endif 37 | # define COMPAT53_INCLUDE_SOURCE 38 | #endif /* COMPAT53_PREFIX */ 39 | 40 | #define COMPAT53_CONCAT_HELPER(a, b) a##b 41 | #define COMPAT53_CONCAT(a, b) COMPAT53_CONCAT_HELPER(a, b) 42 | 43 | 44 | 45 | /* declarations for Lua 5.1 */ 46 | #if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM == 501 47 | 48 | /* XXX not implemented: 49 | * lua_arith (new operators) 50 | * lua_upvalueid 51 | * lua_upvaluejoin 52 | * lua_version 53 | * lua_yieldk 54 | */ 55 | 56 | #ifndef LUA_OK 57 | # define LUA_OK 0 58 | #endif 59 | #ifndef LUA_OPADD 60 | # define LUA_OPADD 0 61 | #endif 62 | #ifndef LUA_OPSUB 63 | # define LUA_OPSUB 1 64 | #endif 65 | #ifndef LUA_OPMUL 66 | # define LUA_OPMUL 2 67 | #endif 68 | #ifndef LUA_OPDIV 69 | # define LUA_OPDIV 3 70 | #endif 71 | #ifndef LUA_OPMOD 72 | # define LUA_OPMOD 4 73 | #endif 74 | #ifndef LUA_OPPOW 75 | # define LUA_OPPOW 5 76 | #endif 77 | #ifndef LUA_OPUNM 78 | # define LUA_OPUNM 6 79 | #endif 80 | #ifndef LUA_OPEQ 81 | # define LUA_OPEQ 0 82 | #endif 83 | #ifndef LUA_OPLT 84 | # define LUA_OPLT 1 85 | #endif 86 | #ifndef LUA_OPLE 87 | # define LUA_OPLE 2 88 | #endif 89 | 90 | /* LuaJIT/Lua 5.1 does not have the updated 91 | * error codes for thread status/function returns (but some patched versions do) 92 | * define it only if it's not found 93 | */ 94 | #if !defined(LUA_ERRGCMM) 95 | /* Use + 2 because in some versions of Lua (Lua 5.1) 96 | * LUA_ERRFILE is defined as (LUA_ERRERR+1) 97 | * so we need to avoid it (LuaJIT might have something at this 98 | * integer value too) 99 | */ 100 | # define LUA_ERRGCMM (LUA_ERRERR + 2) 101 | #endif /* LUA_ERRGCMM define */ 102 | 103 | typedef size_t lua_Unsigned; 104 | 105 | typedef struct luaL_Buffer_53 { 106 | luaL_Buffer b; /* make incorrect code crash! */ 107 | char *ptr; 108 | size_t nelems; 109 | size_t capacity; 110 | lua_State *L2; 111 | } luaL_Buffer_53; 112 | #define luaL_Buffer luaL_Buffer_53 113 | 114 | /* In PUC-Rio 5.1, userdata is a simple FILE* 115 | * In LuaJIT, it's a struct where the first member is a FILE* 116 | * We can't support the `closef` member 117 | */ 118 | typedef struct luaL_Stream { 119 | FILE *f; 120 | } luaL_Stream; 121 | 122 | #define lua_absindex COMPAT53_CONCAT(COMPAT53_PREFIX, _absindex) 123 | COMPAT53_API int lua_absindex (lua_State *L, int i); 124 | 125 | #define lua_arith COMPAT53_CONCAT(COMPAT53_PREFIX, _arith) 126 | COMPAT53_API void lua_arith (lua_State *L, int op); 127 | 128 | #define lua_compare COMPAT53_CONCAT(COMPAT53_PREFIX, _compare) 129 | COMPAT53_API int lua_compare (lua_State *L, int idx1, int idx2, int op); 130 | 131 | #define lua_copy COMPAT53_CONCAT(COMPAT53_PREFIX, _copy) 132 | COMPAT53_API void lua_copy (lua_State *L, int from, int to); 133 | 134 | #define lua_getuservalue(L, i) \ 135 | (lua_getfenv((L), (i)), lua_type((L), -1)) 136 | #define lua_setuservalue(L, i) \ 137 | (luaL_checktype((L), -1, LUA_TTABLE), lua_setfenv((L), (i))) 138 | 139 | #define lua_len COMPAT53_CONCAT(COMPAT53_PREFIX, _len) 140 | COMPAT53_API void lua_len (lua_State *L, int i); 141 | 142 | #define lua_pushstring(L, s) \ 143 | (lua_pushstring((L), (s)), lua_tostring((L), -1)) 144 | 145 | #ifndef luaL_newlibtable 146 | # define luaL_newlibtable(L, l) \ 147 | (lua_createtable((L), 0, sizeof((l))/sizeof(*(l))-1)) 148 | #endif 149 | #ifndef luaL_newlib 150 | # define luaL_newlib(L, l) \ 151 | (luaL_newlibtable((L), (l)), luaL_register((L), NULL, (l))) 152 | #endif 153 | 154 | #define lua_pushglobaltable(L) \ 155 | lua_pushvalue((L), LUA_GLOBALSINDEX) 156 | 157 | #define lua_rawgetp COMPAT53_CONCAT(COMPAT53_PREFIX, _rawgetp) 158 | COMPAT53_API int lua_rawgetp (lua_State *L, int i, const void *p); 159 | 160 | #define lua_rawsetp COMPAT53_CONCAT(COMPAT53_PREFIX, _rawsetp) 161 | COMPAT53_API void lua_rawsetp(lua_State *L, int i, const void *p); 162 | 163 | #define lua_rawlen(L, i) lua_objlen((L), (i)) 164 | 165 | #define lua_tointeger(L, i) lua_tointegerx((L), (i), NULL) 166 | 167 | #define lua_tonumberx COMPAT53_CONCAT(COMPAT53_PREFIX, _tonumberx) 168 | COMPAT53_API lua_Number lua_tonumberx (lua_State *L, int i, int *isnum); 169 | 170 | #define luaL_checkversion COMPAT53_CONCAT(COMPAT53_PREFIX, L_checkversion) 171 | COMPAT53_API void luaL_checkversion (lua_State *L); 172 | 173 | #define lua_load COMPAT53_CONCAT(COMPAT53_PREFIX, _load_53) 174 | COMPAT53_API int lua_load (lua_State *L, lua_Reader reader, void *data, const char* source, const char* mode); 175 | 176 | #define luaL_loadfilex COMPAT53_CONCAT(COMPAT53_PREFIX, L_loadfilex) 177 | COMPAT53_API int luaL_loadfilex (lua_State *L, const char *filename, const char *mode); 178 | 179 | #define luaL_loadbufferx COMPAT53_CONCAT(COMPAT53_PREFIX, L_loadbufferx) 180 | COMPAT53_API int luaL_loadbufferx (lua_State *L, const char *buff, size_t sz, const char *name, const char *mode); 181 | 182 | #define luaL_checkstack COMPAT53_CONCAT(COMPAT53_PREFIX, L_checkstack_53) 183 | COMPAT53_API void luaL_checkstack (lua_State *L, int sp, const char *msg); 184 | 185 | #define luaL_getsubtable COMPAT53_CONCAT(COMPAT53_PREFIX, L_getsubtable) 186 | COMPAT53_API int luaL_getsubtable (lua_State* L, int i, const char *name); 187 | 188 | #define luaL_len COMPAT53_CONCAT(COMPAT53_PREFIX, L_len) 189 | COMPAT53_API lua_Integer luaL_len (lua_State *L, int i); 190 | 191 | #define luaL_setfuncs COMPAT53_CONCAT(COMPAT53_PREFIX, L_setfuncs) 192 | COMPAT53_API void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup); 193 | 194 | #define luaL_setmetatable COMPAT53_CONCAT(COMPAT53_PREFIX, L_setmetatable) 195 | COMPAT53_API void luaL_setmetatable (lua_State *L, const char *tname); 196 | 197 | #define luaL_testudata COMPAT53_CONCAT(COMPAT53_PREFIX, L_testudata) 198 | COMPAT53_API void *luaL_testudata (lua_State *L, int i, const char *tname); 199 | 200 | #define luaL_traceback COMPAT53_CONCAT(COMPAT53_PREFIX, L_traceback) 201 | COMPAT53_API void luaL_traceback (lua_State *L, lua_State *L1, const char *msg, int level); 202 | 203 | #define luaL_fileresult COMPAT53_CONCAT(COMPAT53_PREFIX, L_fileresult) 204 | COMPAT53_API int luaL_fileresult (lua_State *L, int stat, const char *fname); 205 | 206 | #define luaL_execresult COMPAT53_CONCAT(COMPAT53_PREFIX, L_execresult) 207 | COMPAT53_API int luaL_execresult (lua_State *L, int stat); 208 | 209 | #define lua_callk(L, na, nr, ctx, cont) \ 210 | ((void)(ctx), (void)(cont), lua_call((L), (na), (nr))) 211 | #define lua_pcallk(L, na, nr, err, ctx, cont) \ 212 | ((void)(ctx), (void)(cont), lua_pcall((L), (na), (nr), (err))) 213 | 214 | #define lua_resume(L, from, nargs) \ 215 | ((void)(from), lua_resume((L), (nargs))) 216 | 217 | #define luaL_buffinit COMPAT53_CONCAT(COMPAT53_PREFIX, _buffinit_53) 218 | COMPAT53_API void luaL_buffinit (lua_State *L, luaL_Buffer_53 *B); 219 | 220 | #define luaL_prepbuffsize COMPAT53_CONCAT(COMPAT53_PREFIX, _prepbufsize_53) 221 | COMPAT53_API char *luaL_prepbuffsize (luaL_Buffer_53 *B, size_t s); 222 | 223 | #define luaL_addlstring COMPAT53_CONCAT(COMPAT53_PREFIX, _addlstring_53) 224 | COMPAT53_API void luaL_addlstring (luaL_Buffer_53 *B, const char *s, size_t l); 225 | 226 | #define luaL_addvalue COMPAT53_CONCAT(COMPAT53_PREFIX, _addvalue_53) 227 | COMPAT53_API void luaL_addvalue (luaL_Buffer_53 *B); 228 | 229 | #define luaL_pushresult COMPAT53_CONCAT(COMPAT53_PREFIX, _pushresult_53) 230 | COMPAT53_API void luaL_pushresult (luaL_Buffer_53 *B); 231 | 232 | #undef luaL_buffinitsize 233 | #define luaL_buffinitsize(L, B, s) \ 234 | (luaL_buffinit((L), (B)), luaL_prepbuffsize((B), (s))) 235 | 236 | #undef luaL_prepbuffer 237 | #define luaL_prepbuffer(B) \ 238 | luaL_prepbuffsize((B), LUAL_BUFFERSIZE) 239 | 240 | #undef luaL_addchar 241 | #define luaL_addchar(B, c) \ 242 | ((void)((B)->nelems < (B)->capacity || luaL_prepbuffsize((B), 1)), \ 243 | ((B)->ptr[(B)->nelems++] = (c))) 244 | 245 | #undef luaL_addsize 246 | #define luaL_addsize(B, s) \ 247 | ((B)->nelems += (s)) 248 | 249 | #undef luaL_addstring 250 | #define luaL_addstring(B, s) \ 251 | luaL_addlstring((B), (s), strlen((s))) 252 | 253 | #undef luaL_pushresultsize 254 | #define luaL_pushresultsize(B, s) \ 255 | (luaL_addsize((B), (s)), luaL_pushresult((B))) 256 | 257 | #if defined(LUA_COMPAT_APIINTCASTS) 258 | #define lua_pushunsigned(L, n) \ 259 | lua_pushinteger((L), (lua_Integer)(n)) 260 | #define lua_tounsignedx(L, i, is) \ 261 | ((lua_Unsigned)lua_tointegerx((L), (i), (is))) 262 | #define lua_tounsigned(L, i) \ 263 | lua_tounsignedx((L), (i), NULL) 264 | #define luaL_checkunsigned(L, a) \ 265 | ((lua_Unsigned)luaL_checkinteger((L), (a))) 266 | #define luaL_optunsigned(L, a, d) \ 267 | ((lua_Unsigned)luaL_optinteger((L), (a), (lua_Integer)(d))) 268 | #endif 269 | 270 | #endif /* Lua 5.1 only */ 271 | 272 | 273 | 274 | /* declarations for Lua 5.1 and 5.2 */ 275 | #if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM <= 502 276 | 277 | typedef int lua_KContext; 278 | 279 | typedef int (*lua_KFunction)(lua_State *L, int status, lua_KContext ctx); 280 | 281 | #define lua_dump(L, w, d, s) \ 282 | ((void)(s), lua_dump((L), (w), (d))) 283 | 284 | #define lua_getfield(L, i, k) \ 285 | (lua_getfield((L), (i), (k)), lua_type((L), -1)) 286 | 287 | #define lua_gettable(L, i) \ 288 | (lua_gettable((L), (i)), lua_type((L), -1)) 289 | 290 | #define lua_pushlstring COMPAT53_CONCAT(COMPAT53_PREFIX, _pushlstring_53) 291 | COMPAT53_API const char *lua_pushlstring (lua_State *L, const char *s, size_t len); 292 | 293 | #define lua_geti COMPAT53_CONCAT(COMPAT53_PREFIX, _geti) 294 | COMPAT53_API int lua_geti (lua_State *L, int index, lua_Integer i); 295 | 296 | #define lua_getextraspace COMPAT53_CONCAT(COMPAT53_PREFIX, _getextraspace) 297 | COMPAT53_API void *lua_getextraspace (lua_State *L); 298 | 299 | #define lua_isinteger COMPAT53_CONCAT(COMPAT53_PREFIX, _isinteger) 300 | COMPAT53_API int lua_isinteger (lua_State *L, int index); 301 | 302 | #define lua_tointegerx COMPAT53_CONCAT(COMPAT53_PREFIX, _tointegerx_53) 303 | COMPAT53_API lua_Integer lua_tointegerx (lua_State *L, int i, int *isnum); 304 | 305 | #define lua_numbertointeger(n, p) \ 306 | ((*(p) = (lua_Integer)(n)), 1) 307 | 308 | #define lua_rawget(L, i) \ 309 | (lua_rawget((L), (i)), lua_type((L), -1)) 310 | 311 | #define lua_rawgeti(L, i, n) \ 312 | (lua_rawgeti((L), (i), (n)), lua_type((L), -1)) 313 | 314 | #define lua_rotate COMPAT53_CONCAT(COMPAT53_PREFIX, _rotate) 315 | COMPAT53_API void lua_rotate (lua_State *L, int idx, int n); 316 | 317 | #define lua_seti COMPAT53_CONCAT(COMPAT53_PREFIX, _seti) 318 | COMPAT53_API void lua_seti (lua_State *L, int index, lua_Integer i); 319 | 320 | #define lua_stringtonumber COMPAT53_CONCAT(COMPAT53_PREFIX, _stringtonumber) 321 | COMPAT53_API size_t lua_stringtonumber (lua_State *L, const char *s); 322 | 323 | #define luaL_tolstring COMPAT53_CONCAT(COMPAT53_PREFIX, L_tolstring) 324 | COMPAT53_API const char *luaL_tolstring (lua_State *L, int idx, size_t *len); 325 | 326 | #define luaL_getmetafield(L, o, e) \ 327 | (luaL_getmetafield((L), (o), (e)) ? lua_type((L), -1) : LUA_TNIL) 328 | 329 | #define luaL_newmetatable(L, tn) \ 330 | (luaL_newmetatable((L), (tn)) ? (lua_pushstring((L), (tn)), lua_setfield((L), -2, "__name"), 1) : 0) 331 | 332 | #define luaL_requiref COMPAT53_CONCAT(COMPAT53_PREFIX, L_requiref_53) 333 | COMPAT53_API void luaL_requiref (lua_State *L, const char *modname, 334 | lua_CFunction openf, int glb ); 335 | 336 | #endif /* Lua 5.1 and Lua 5.2 */ 337 | 338 | 339 | 340 | /* declarations for Lua 5.2 */ 341 | #if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM == 502 342 | 343 | /* XXX not implemented: 344 | * lua_isyieldable 345 | * lua_arith (new operators) 346 | * lua_pushfstring (new formats) 347 | */ 348 | 349 | #define lua_getglobal(L, n) \ 350 | (lua_getglobal((L), (n)), lua_type((L), -1)) 351 | 352 | #define lua_getuservalue(L, i) \ 353 | (lua_getuservalue((L), (i)), lua_type((L), -1)) 354 | 355 | #define lua_rawgetp(L, i, p) \ 356 | (lua_rawgetp((L), (i), (p)), lua_type((L), -1)) 357 | 358 | #define LUA_KFUNCTION(_name) \ 359 | static int (_name)(lua_State *L, int status, lua_KContext ctx); \ 360 | static int (_name ## _52)(lua_State *L) { \ 361 | lua_KContext ctx; \ 362 | int status = lua_getctx(L, &ctx); \ 363 | return (_name)(L, status, ctx); \ 364 | } \ 365 | static int (_name)(lua_State *L, int status, lua_KContext ctx) 366 | 367 | #define lua_pcallk(L, na, nr, err, ctx, cont) \ 368 | lua_pcallk((L), (na), (nr), (err), (ctx), cont ## _52) 369 | 370 | #define lua_callk(L, na, nr, ctx, cont) \ 371 | lua_callk((L), (na), (nr), (ctx), cont ## _52) 372 | 373 | #define lua_yieldk(L, nr, ctx, cont) \ 374 | lua_yieldk((L), (nr), (ctx), cont ## _52) 375 | 376 | #ifdef lua_call 377 | # undef lua_call 378 | # define lua_call(L, na, nr) \ 379 | (lua_callk)((L), (na), (nr), 0, NULL) 380 | #endif 381 | 382 | #ifdef lua_pcall 383 | # undef lua_pcall 384 | # define lua_pcall(L, na, nr, err) \ 385 | (lua_pcallk)((L), (na), (nr), (err), 0, NULL) 386 | #endif 387 | 388 | #ifdef lua_yield 389 | # undef lua_yield 390 | # define lua_yield(L, nr) \ 391 | (lua_yieldk)((L), (nr), 0, NULL) 392 | #endif 393 | 394 | #endif /* Lua 5.2 only */ 395 | 396 | 397 | 398 | /* other Lua versions */ 399 | #if !defined(LUA_VERSION_NUM) || LUA_VERSION_NUM < 501 || LUA_VERSION_NUM > 504 400 | 401 | # error "unsupported Lua version (i.e. not Lua 5.1, 5.2, 5.3, or 5.4)" 402 | 403 | #endif /* other Lua versions except 5.1, 5.2, 5.3, and 5.4 */ 404 | 405 | 406 | 407 | /* helper macro for defining continuation functions (for every version 408 | * *except* Lua 5.2) */ 409 | #ifndef LUA_KFUNCTION 410 | #define LUA_KFUNCTION(_name) \ 411 | static int (_name)(lua_State *L, int status, lua_KContext ctx) 412 | #endif 413 | 414 | 415 | #if defined(COMPAT53_INCLUDE_SOURCE) 416 | # include "compat-5.3.c" 417 | #endif 418 | 419 | 420 | #endif /* COMPAT53_H_ */ 421 | 422 | -------------------------------------------------------------------------------- /lib/fenster/fenster.h: -------------------------------------------------------------------------------- 1 | #ifndef FENSTER_H 2 | #define FENSTER_H 3 | 4 | #if defined(__APPLE__) 5 | #include 6 | #include 7 | #include 8 | #elif defined(_WIN32) 9 | #include 10 | #else 11 | #define _DEFAULT_SOURCE 1 12 | #include 13 | #include 14 | #include 15 | #include 16 | #endif 17 | 18 | #include 19 | #include 20 | 21 | struct fenster { 22 | const char *title; 23 | const int width; 24 | const int height; 25 | uint32_t *buf; 26 | int keys[256]; /* keys are mostly ASCII, but arrows are 17..20 */ 27 | int mod; /* mod is 4 bits mask, ctrl=1, shift=2, alt=4, meta=8 */ 28 | int x; 29 | int y; 30 | int mouse; 31 | #if defined(__APPLE__) 32 | id wnd; 33 | #elif defined(_WIN32) 34 | HWND hwnd; 35 | #else 36 | Display *dpy; 37 | Window w; 38 | GC gc; 39 | XImage *img; 40 | #endif 41 | }; 42 | 43 | #ifndef FENSTER_API 44 | #define FENSTER_API extern 45 | #endif 46 | FENSTER_API int fenster_open(struct fenster *f); 47 | FENSTER_API int fenster_loop(struct fenster *f); 48 | FENSTER_API void fenster_close(struct fenster *f); 49 | FENSTER_API void fenster_sleep(int64_t ms); 50 | FENSTER_API int64_t fenster_time(void); 51 | #define fenster_pixel(f, x, y) ((f)->buf[((y) * (f)->width) + (x)]) 52 | 53 | #ifndef FENSTER_HEADER 54 | #if defined(__APPLE__) 55 | #define msg(r, o, s) ((r(*)(id, SEL))objc_msgSend)(o, sel_getUid(s)) 56 | #define msg1(r, o, s, A, a) \ 57 | ((r(*)(id, SEL, A))objc_msgSend)(o, sel_getUid(s), a) 58 | #define msg2(r, o, s, A, a, B, b) \ 59 | ((r(*)(id, SEL, A, B))objc_msgSend)(o, sel_getUid(s), a, b) 60 | #define msg3(r, o, s, A, a, B, b, C, c) \ 61 | ((r(*)(id, SEL, A, B, C))objc_msgSend)(o, sel_getUid(s), a, b, c) 62 | #define msg4(r, o, s, A, a, B, b, C, c, D, d) \ 63 | ((r(*)(id, SEL, A, B, C, D))objc_msgSend)(o, sel_getUid(s), a, b, c, d) 64 | 65 | #define cls(x) ((id)objc_getClass(x)) 66 | 67 | extern id const NSDefaultRunLoopMode; 68 | extern id const NSApp; 69 | 70 | static void fenster_draw_rect(id v, SEL s, CGRect r) { 71 | (void)r, (void)s; 72 | struct fenster *f = (struct fenster *)objc_getAssociatedObject(v, "fenster"); 73 | CGContextRef context = 74 | msg(CGContextRef, msg(id, cls("NSGraphicsContext"), "currentContext"), 75 | "graphicsPort"); 76 | CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB(); 77 | CGDataProviderRef provider = CGDataProviderCreateWithData( 78 | NULL, f->buf, f->width * f->height * 4, NULL); 79 | CGImageRef img = 80 | CGImageCreate(f->width, f->height, 8, 32, f->width * 4, space, 81 | kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little, 82 | provider, NULL, false, kCGRenderingIntentDefault); 83 | CGColorSpaceRelease(space); 84 | CGDataProviderRelease(provider); 85 | CGContextDrawImage(context, CGRectMake(0, 0, f->width, f->height), img); 86 | CGImageRelease(img); 87 | } 88 | 89 | static BOOL fenster_should_close(id v, SEL s, id w) { 90 | (void)v, (void)s, (void)w; 91 | msg1(void, NSApp, "terminate:", id, NSApp); 92 | return YES; 93 | } 94 | 95 | FENSTER_API int fenster_open(struct fenster *f) { 96 | msg(id, cls("NSApplication"), "sharedApplication"); 97 | msg1(void, NSApp, "setActivationPolicy:", NSInteger, 0); 98 | f->wnd = msg4(id, msg(id, cls("NSWindow"), "alloc"), 99 | "initWithContentRect:styleMask:backing:defer:", CGRect, 100 | CGRectMake(0, 0, f->width, f->height), NSUInteger, 3, 101 | NSUInteger, 2, BOOL, NO); 102 | Class windelegate = 103 | objc_allocateClassPair((Class)cls("NSObject"), "FensterDelegate", 0); 104 | class_addMethod(windelegate, sel_getUid("windowShouldClose:"), 105 | (IMP)fenster_should_close, "c@:@"); 106 | objc_registerClassPair(windelegate); 107 | msg1(void, f->wnd, "setDelegate:", id, 108 | msg(id, msg(id, (id)windelegate, "alloc"), "init")); 109 | Class c = objc_allocateClassPair((Class)cls("NSView"), "FensterView", 0); 110 | class_addMethod(c, sel_getUid("drawRect:"), (IMP)fenster_draw_rect, "i@:@@"); 111 | objc_registerClassPair(c); 112 | 113 | id v = msg(id, msg(id, (id)c, "alloc"), "init"); 114 | msg1(void, f->wnd, "setContentView:", id, v); 115 | objc_setAssociatedObject(v, "fenster", (id)f, OBJC_ASSOCIATION_ASSIGN); 116 | 117 | id title = msg1(id, cls("NSString"), "stringWithUTF8String:", const char *, 118 | f->title); 119 | msg1(void, f->wnd, "setTitle:", id, title); 120 | msg1(void, f->wnd, "makeKeyAndOrderFront:", id, nil); 121 | msg(void, f->wnd, "center"); 122 | msg1(void, NSApp, "activateIgnoringOtherApps:", BOOL, YES); 123 | return 0; 124 | } 125 | 126 | FENSTER_API void fenster_close(struct fenster *f) { 127 | msg(void, f->wnd, "close"); 128 | } 129 | 130 | // clang-format off 131 | static const uint8_t FENSTER_KEYCODES[128] = {65,83,68,70,72,71,90,88,67,86,0,66,81,87,69,82,89,84,49,50,51,52,54,53,61,57,55,45,56,48,93,79,85,91,73,80,10,76,74,39,75,59,92,44,47,78,77,46,9,32,96,8,0,27,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,2,3,127,0,5,0,4,0,20,19,18,17,0}; 132 | // clang-format on 133 | FENSTER_API int fenster_loop(struct fenster *f) { 134 | msg1(void, msg(id, f->wnd, "contentView"), "setNeedsDisplay:", BOOL, YES); 135 | id ev = msg4(id, NSApp, 136 | "nextEventMatchingMask:untilDate:inMode:dequeue:", NSUInteger, 137 | NSUIntegerMax, id, NULL, id, NSDefaultRunLoopMode, BOOL, YES); 138 | if (!ev) 139 | return 0; 140 | NSUInteger evtype = msg(NSUInteger, ev, "type"); 141 | switch (evtype) { 142 | case 1: /* NSEventTypeMouseDown */ 143 | f->mouse |= 1; 144 | break; 145 | case 2: /* NSEventTypeMouseUp*/ 146 | f->mouse &= ~1; 147 | break; 148 | case 5: 149 | case 6: { /* NSEventTypeMouseMoved */ 150 | CGPoint xy = msg(CGPoint, ev, "locationInWindow"); 151 | f->x = (int)xy.x; 152 | f->y = (int)(f->height - xy.y); 153 | return 0; 154 | } 155 | case 10: /*NSEventTypeKeyDown*/ 156 | case 11: /*NSEventTypeKeyUp:*/ { 157 | NSUInteger k = msg(NSUInteger, ev, "keyCode"); 158 | f->keys[k < 127 ? FENSTER_KEYCODES[k] : 0] = evtype == 10; 159 | NSUInteger mod = msg(NSUInteger, ev, "modifierFlags") >> 17; 160 | f->mod = (mod & 0xc) | ((mod & 1) << 1) | ((mod >> 1) & 1); 161 | return 0; 162 | } 163 | } 164 | msg1(void, NSApp, "sendEvent:", id, ev); 165 | return 0; 166 | } 167 | #elif defined(_WIN32) 168 | // clang-format off 169 | static const uint8_t FENSTER_KEYCODES[] = {0,27,49,50,51,52,53,54,55,56,57,48,45,61,8,9,81,87,69,82,84,89,85,73,79,80,91,93,10,0,65,83,68,70,71,72,74,75,76,59,39,96,0,92,90,88,67,86,66,78,77,44,46,47,0,0,0,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,17,3,0,20,0,19,0,5,18,4,26,127}; 170 | // clang-format on 171 | typedef struct BINFO{ 172 | BITMAPINFOHEADER bmiHeader; 173 | RGBQUAD bmiColors[3]; 174 | }BINFO; 175 | static LRESULT CALLBACK fenster_wndproc(HWND hwnd, UINT msg, WPARAM wParam, 176 | LPARAM lParam) { 177 | struct fenster *f = (struct fenster *)GetWindowLongPtr(hwnd, GWLP_USERDATA); 178 | switch (msg) { 179 | case WM_PAINT: { 180 | PAINTSTRUCT ps; 181 | HDC hdc = BeginPaint(hwnd, &ps); 182 | HDC memdc = CreateCompatibleDC(hdc); 183 | HBITMAP hbmp = CreateCompatibleBitmap(hdc, f->width, f->height); 184 | HBITMAP oldbmp = SelectObject(memdc, hbmp); 185 | BINFO bi = {{sizeof(bi), f->width, -f->height, 1, 32, BI_BITFIELDS}}; 186 | bi.bmiColors[0].rgbRed = 0xff; 187 | bi.bmiColors[1].rgbGreen = 0xff; 188 | bi.bmiColors[2].rgbBlue = 0xff; 189 | SetDIBitsToDevice(memdc, 0, 0, f->width, f->height, 0, 0, 0, f->height, 190 | f->buf, (BITMAPINFO *)&bi, DIB_RGB_COLORS); 191 | BitBlt(hdc, 0, 0, f->width, f->height, memdc, 0, 0, SRCCOPY); 192 | SelectObject(memdc, oldbmp); 193 | DeleteObject(hbmp); 194 | DeleteDC(memdc); 195 | EndPaint(hwnd, &ps); 196 | } break; 197 | case WM_CLOSE: 198 | DestroyWindow(hwnd); 199 | break; 200 | case WM_LBUTTONDOWN: 201 | case WM_LBUTTONUP: 202 | f->mouse = (msg == WM_LBUTTONDOWN); 203 | break; 204 | case WM_MOUSEMOVE: 205 | f->y = HIWORD(lParam), f->x = LOWORD(lParam); 206 | break; 207 | case WM_KEYDOWN: 208 | case WM_KEYUP: { 209 | f->mod = ((GetKeyState(VK_CONTROL) & 0x8000) >> 15) | 210 | ((GetKeyState(VK_SHIFT) & 0x8000) >> 14) | 211 | ((GetKeyState(VK_MENU) & 0x8000) >> 13) | 212 | (((GetKeyState(VK_LWIN) | GetKeyState(VK_RWIN)) & 0x8000) >> 12); 213 | f->keys[FENSTER_KEYCODES[HIWORD(lParam) & 0x1ff]] = !((lParam >> 31) & 1); 214 | } break; 215 | case WM_DESTROY: 216 | PostQuitMessage(0); 217 | break; 218 | default: 219 | return DefWindowProc(hwnd, msg, wParam, lParam); 220 | } 221 | return 0; 222 | } 223 | 224 | FENSTER_API int fenster_open(struct fenster *f) { 225 | HINSTANCE hInstance = GetModuleHandle(NULL); 226 | WNDCLASSEX wc = {0}; 227 | wc.cbSize = sizeof(WNDCLASSEX); 228 | wc.style = CS_VREDRAW | CS_HREDRAW; 229 | wc.lpfnWndProc = fenster_wndproc; 230 | wc.hInstance = hInstance; 231 | wc.lpszClassName = f->title; 232 | RegisterClassEx(&wc); 233 | f->hwnd = CreateWindowEx(WS_EX_CLIENTEDGE, f->title, f->title, 234 | WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 235 | f->width, f->height, NULL, NULL, hInstance, NULL); 236 | 237 | if (f->hwnd == NULL) 238 | return -1; 239 | SetWindowLongPtr(f->hwnd, GWLP_USERDATA, (LONG_PTR)f); 240 | ShowWindow(f->hwnd, SW_NORMAL); 241 | UpdateWindow(f->hwnd); 242 | return 0; 243 | } 244 | 245 | FENSTER_API void fenster_close(struct fenster *f) { (void)f; } 246 | 247 | FENSTER_API int fenster_loop(struct fenster *f) { 248 | MSG msg; 249 | while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { 250 | if (msg.message == WM_QUIT) 251 | return -1; 252 | TranslateMessage(&msg); 253 | DispatchMessage(&msg); 254 | } 255 | InvalidateRect(f->hwnd, NULL, TRUE); 256 | return 0; 257 | } 258 | #else 259 | // clang-format off 260 | static int FENSTER_KEYCODES[124] = {XK_BackSpace,8,XK_Delete,127,XK_Down,18,XK_End,5,XK_Escape,27,XK_Home,2,XK_Insert,26,XK_Left,20,XK_Page_Down,4,XK_Page_Up,3,XK_Return,10,XK_Right,19,XK_Tab,9,XK_Up,17,XK_apostrophe,39,XK_backslash,92,XK_bracketleft,91,XK_bracketright,93,XK_comma,44,XK_equal,61,XK_grave,96,XK_minus,45,XK_period,46,XK_semicolon,59,XK_slash,47,XK_space,32,XK_a,65,XK_b,66,XK_c,67,XK_d,68,XK_e,69,XK_f,70,XK_g,71,XK_h,72,XK_i,73,XK_j,74,XK_k,75,XK_l,76,XK_m,77,XK_n,78,XK_o,79,XK_p,80,XK_q,81,XK_r,82,XK_s,83,XK_t,84,XK_u,85,XK_v,86,XK_w,87,XK_x,88,XK_y,89,XK_z,90,XK_0,48,XK_1,49,XK_2,50,XK_3,51,XK_4,52,XK_5,53,XK_6,54,XK_7,55,XK_8,56,XK_9,57}; 261 | // clang-format on 262 | FENSTER_API int fenster_open(struct fenster *f) { 263 | f->dpy = XOpenDisplay(NULL); 264 | int screen = DefaultScreen(f->dpy); 265 | f->w = XCreateSimpleWindow(f->dpy, RootWindow(f->dpy, screen), 0, 0, f->width, 266 | f->height, 0, BlackPixel(f->dpy, screen), 267 | WhitePixel(f->dpy, screen)); 268 | f->gc = XCreateGC(f->dpy, f->w, 0, 0); 269 | XSelectInput(f->dpy, f->w, 270 | ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask | 271 | ButtonReleaseMask | PointerMotionMask); 272 | XStoreName(f->dpy, f->w, f->title); 273 | XMapWindow(f->dpy, f->w); 274 | XSync(f->dpy, f->w); 275 | f->img = XCreateImage(f->dpy, DefaultVisual(f->dpy, 0), 24, ZPixmap, 0, 276 | (char *)f->buf, f->width, f->height, 32, 0); 277 | return 0; 278 | } 279 | FENSTER_API void fenster_close(struct fenster *f) { XCloseDisplay(f->dpy); } 280 | FENSTER_API int fenster_loop(struct fenster *f) { 281 | XEvent ev; 282 | XPutImage(f->dpy, f->w, f->gc, f->img, 0, 0, 0, 0, f->width, f->height); 283 | XFlush(f->dpy); 284 | while (XPending(f->dpy)) { 285 | XNextEvent(f->dpy, &ev); 286 | switch (ev.type) { 287 | case ButtonPress: 288 | case ButtonRelease: 289 | f->mouse = (ev.type == ButtonPress); 290 | break; 291 | case MotionNotify: 292 | f->x = ev.xmotion.x, f->y = ev.xmotion.y; 293 | break; 294 | case KeyPress: 295 | case KeyRelease: { 296 | int m = ev.xkey.state; 297 | int k = XkbKeycodeToKeysym(f->dpy, ev.xkey.keycode, 0, 0); 298 | for (unsigned int i = 0; i < 124; i += 2) { 299 | if (FENSTER_KEYCODES[i] == k) { 300 | f->keys[FENSTER_KEYCODES[i + 1]] = (ev.type == KeyPress); 301 | break; 302 | } 303 | } 304 | f->mod = (!!(m & ControlMask)) | (!!(m & ShiftMask) << 1) | 305 | (!!(m & Mod1Mask) << 2) | (!!(m & Mod4Mask) << 3); 306 | } break; 307 | } 308 | } 309 | return 0; 310 | } 311 | #endif 312 | 313 | #ifdef _WIN32 314 | FENSTER_API void fenster_sleep(int64_t ms) { Sleep(ms); } 315 | FENSTER_API int64_t fenster_time() { 316 | LARGE_INTEGER freq, count; 317 | QueryPerformanceFrequency(&freq); 318 | QueryPerformanceCounter(&count); 319 | return (int64_t)(count.QuadPart * 1000.0 / freq.QuadPart); 320 | } 321 | #else 322 | FENSTER_API void fenster_sleep(int64_t ms) { 323 | struct timespec ts; 324 | ts.tv_sec = ms / 1000; 325 | ts.tv_nsec = (ms % 1000) * 1000000; 326 | nanosleep(&ts, NULL); 327 | } 328 | FENSTER_API int64_t fenster_time(void) { 329 | struct timespec time; 330 | clock_gettime(CLOCK_REALTIME, &time); 331 | return time.tv_sec * 1000 + (time.tv_nsec / 1000000); 332 | } 333 | #endif 334 | 335 | #ifdef __cplusplus 336 | class Fenster { 337 | struct fenster f; 338 | int64_t now; 339 | 340 | public: 341 | Fenster(const int w, const int h, const char *title) 342 | : f{.title = title, .width = w, .height = h} { 343 | this->f.buf = new uint32_t[w * h]; 344 | this->now = fenster_time(); 345 | fenster_open(&this->f); 346 | } 347 | ~Fenster() { 348 | fenster_close(&this->f); 349 | delete[] this->f.buf; 350 | } 351 | bool loop(const int fps) { 352 | int64_t t = fenster_time(); 353 | if (t - this->now < 1000 / fps) { 354 | fenster_sleep(t - now); 355 | } 356 | this->now = t; 357 | return fenster_loop(&this->f) == 0; 358 | } 359 | inline uint32_t &px(const int x, const int y) { 360 | return fenster_pixel(&this->f, x, y); 361 | } 362 | bool key(int c) { return c >= 0 && c < 128 ? this->f.keys[c] : false; } 363 | int x() { return this->f.x; } 364 | int y() { return this->f.y; } 365 | int mouse() { return this->f.mouse; } 366 | int mod() { return this->f.mod; } 367 | }; 368 | #endif /* __cplusplus */ 369 | 370 | #endif /* !FENSTER_HEADER */ 371 | #endif /* FENSTER_H */ 372 | -------------------------------------------------------------------------------- /spec/main_spec.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: param-type-mismatch, missing-fields, missing-parameter, discard-returns 2 | describe('fenster', function() 3 | local fenster = require('fenster') 4 | 5 | describe('fenster.open(...)', function() 6 | it('should throw when no arguments were given', function() 7 | assert.has_error(function() fenster.open() end) 8 | end) 9 | 10 | it('should throw when not enough arguments were given', function() 11 | assert.has_error(function() fenster.open(256) end) 12 | end) 13 | 14 | it('should throw when width/height are not integers', function() 15 | assert.has_error(function() fenster.open('ERROR') end) 16 | assert.has_error(function() fenster.open(true) end) 17 | assert.has_error(function() fenster.open({}) end) 18 | assert.has_error(function() fenster.open(function() end) end) 19 | assert.has_error(function() fenster.open(io.stdout) end) 20 | assert.has_error(function() fenster.open(100.5) end) 21 | 22 | assert.has_error(function() fenster.open(256, 'ERROR') end) 23 | assert.has_error(function() fenster.open(256, true) end) 24 | assert.has_error(function() fenster.open(256, {}) end) 25 | assert.has_error(function() fenster.open(256, function() end) end) 26 | assert.has_error(function() fenster.open(256, io.stdout) end) 27 | assert.has_error(function() fenster.open(256, 100.5) end) 28 | end) 29 | 30 | it('should throw when width/height are out of range', function() 31 | assert.has_error(function() fenster.open(100, -1) end) 32 | assert.has_error(function() fenster.open(-1, 100) end) 33 | assert.has_error(function() fenster.open(30000, 100) end) 34 | assert.has_error(function() fenster.open(100, 30000) end) 35 | end) 36 | 37 | it('should return a window userdata #needsdisplay', function() 38 | local window = fenster.open(256, 144) 39 | finally(function() window:close() end) 40 | assert.is_userdata(window) 41 | end) 42 | 43 | it('should set a default title when none is given #needsdisplay', function() 44 | local window = fenster.open(256, 144) 45 | local window2 = fenster.open(256, 144, nil) 46 | finally(function() 47 | window:close() 48 | window2:close() 49 | end) 50 | assert.is_string(window.title) 51 | assert.is_true(#window.title > 0) 52 | assert.is_string(window2.title) 53 | assert.is_true(#window2.title > 0) 54 | end) 55 | 56 | it('should throw when title is not a string', function() 57 | assert.has_error(function() fenster.open(256, 144, true) end) 58 | assert.has_error(function() fenster.open(256, 144, {}) end) 59 | assert.has_error(function() fenster.open(256, 144, function() end) end) 60 | assert.has_error(function() fenster.open(256, 144, io.stdout) end) 61 | end) 62 | 63 | it('should set the title #needsdisplay', function() 64 | local window = fenster.open(256, 144, 'Test') 65 | finally(function() window:close() end) 66 | assert.are_equal(window.title, 'Test') 67 | end) 68 | 69 | it('should set a default scale when none is given #needsdisplay', function() 70 | local window = fenster.open(256, 144) 71 | local window2 = fenster.open(256, 144, 'Test', nil) 72 | finally(function() 73 | window:close() 74 | window2:close() 75 | end) 76 | assert.are_equal(window.scale, 1) 77 | assert.are_equal(window2.scale, 1) 78 | end) 79 | 80 | it('should throw when scale is not an integer', function() 81 | assert.has_error(function() fenster.open(256, 144, 'Test', 'ERROR') end) 82 | assert.has_error(function() fenster.open(256, 144, 'Test', true) end) 83 | assert.has_error(function() fenster.open(256, 144, 'Test', {}) end) 84 | assert.has_error(function() fenster.open(256, 144, 'Test', function() end) end) 85 | assert.has_error(function() fenster.open(256, 144, 'Test', io.stdout) end) 86 | assert.has_error(function() fenster.open(256, 144, 'Test', 2.5) end) 87 | end) 88 | 89 | it('should throw when scale is not a power of 2', function() 90 | assert.has_error(function() fenster.open(256, 144, 'Test', -4) end) 91 | assert.has_error(function() fenster.open(256, 144, 'Test', 3) end) 92 | end) 93 | 94 | it('should set the scale #needsdisplay', function() 95 | local window = fenster.open(256, 144, 'Test', 4) 96 | finally(function() window:close() end) 97 | assert.are_equal(window.scale, 4) 98 | end) 99 | 100 | it('should set a default target fps when none is given #needsdisplay', function() 101 | local window = fenster.open(256, 144) 102 | local window2 = fenster.open(256, 144, 'Test', 1, nil) 103 | finally(function() 104 | window:close() 105 | window2:close() 106 | end) 107 | assert.are_equal(window.targetfps, 60.0) 108 | assert.are_equal(window2.targetfps, 60.0) 109 | end) 110 | 111 | it('should throw when target fps is not a number', function() 112 | assert.has_error(function() fenster.open(256, 144, 'Test', 1, 'ERROR') end) 113 | assert.has_error(function() fenster.open(256, 144, 'Test', 1, true) end) 114 | assert.has_error(function() fenster.open(256, 144, 'Test', 1, {}) end) 115 | assert.has_error(function() fenster.open(256, 144, 'Test', 1, function() end) end) 116 | assert.has_error(function() fenster.open(256, 144, 'Test', 1, io.stdout) end) 117 | end) 118 | 119 | it('should throw when target fps is out of range', function() 120 | assert.has_error(function() fenster.open(256, 144, 'Test', 1, -1.0) end) 121 | end) 122 | 123 | it('should set the target fps #needsdisplay', function() 124 | local window = fenster.open(256, 144, 'Test', 1, 30) 125 | local window2 = fenster.open(256, 144, 'Test', 1, 0) 126 | finally(function() 127 | window:close() 128 | window2:close() 129 | end) 130 | assert.are_equal(window.targetfps, 30.0) 131 | assert.are_equal(window2.targetfps, 0.0) 132 | end) 133 | end) 134 | 135 | describe('fenster.sleep(...)', function() 136 | it('should throw when no arguments were given', function() 137 | assert.has_error(function() fenster.sleep() end) 138 | end) 139 | 140 | it('should throw when milliseconds is not an integer', function() 141 | assert.has_error(function() fenster.sleep('ERROR') end) 142 | assert.has_error(function() fenster.sleep(true) end) 143 | assert.has_error(function() fenster.sleep({}) end) 144 | assert.has_error(function() fenster.sleep(function() end) end) 145 | assert.has_error(function() fenster.sleep(io.stdout) end) 146 | assert.has_error(function() fenster.sleep(2.5) end) 147 | end) 148 | 149 | it('should sleep for the given amount of time', function() 150 | local start = fenster.time() 151 | fenster.sleep(2) 152 | local stop = fenster.time() 153 | assert.is_true(stop - start >= 2) 154 | end) 155 | end) 156 | 157 | describe('fenster.time(...)', function() 158 | it('should return the current time in seconds', function() 159 | local time = fenster.time() 160 | assert.is_number(time) 161 | end) 162 | end) 163 | 164 | describe('fenster.rgb(...)', function() 165 | it('should throw when no arguments were given', function() 166 | assert.has_error(function() fenster.rgb() end) 167 | end) 168 | 169 | it('should throw when color is not an integer', function() 170 | assert.has_error(function() fenster.rgb('ERROR') end) 171 | assert.has_error(function() fenster.rgb(true) end) 172 | assert.has_error(function() fenster.rgb({}) end) 173 | assert.has_error(function() fenster.rgb(function() end) end) 174 | assert.has_error(function() fenster.rgb(io.stdout) end) 175 | assert.has_error(function() fenster.rgb(2.5) end) 176 | end) 177 | 178 | it('should throw when color is out of range', function() 179 | assert.has_error(function() fenster.rgb(-1) end) 180 | assert.has_error(function() fenster.rgb(0x1000000) end) 181 | end) 182 | 183 | it('should convert a color to r/g/b', function() 184 | assert.are_same({ fenster.rgb(0x000000) }, { 0, 0, 0 }) 185 | assert.are_same({ fenster.rgb(0x0000ff) }, { 0, 0, 255 }) 186 | assert.are_same({ fenster.rgb(0x00ff00) }, { 0, 255, 0 }) 187 | assert.are_same({ fenster.rgb(0x00ffff) }, { 0, 255, 255 }) 188 | assert.are_same({ fenster.rgb(0xff0000) }, { 255, 0, 0 }) 189 | assert.are_same({ fenster.rgb(0xff00ff) }, { 255, 0, 255 }) 190 | assert.are_same({ fenster.rgb(0xffff00) }, { 255, 255, 0 }) 191 | assert.are_same({ fenster.rgb(0xffffff) }, { 255, 255, 255 }) 192 | assert.are_same({ fenster.rgb(0xbeef99) }, { 190, 239, 153 }) 193 | end) 194 | 195 | it('should throw when r/g/b are not integers', function() 196 | assert.has_error(function() fenster.rgb('ERROR', 0, 0) end) 197 | assert.has_error(function() fenster.rgb(true, 0, 0) end) 198 | assert.has_error(function() fenster.rgb({}, 0, 0) end) 199 | assert.has_error(function() fenster.rgb(function() end, 0, 0) end) 200 | assert.has_error(function() fenster.rgb(io.stdout) end) 201 | assert.has_error(function() fenster.rgb(255.5, 0, 0) end) 202 | 203 | assert.has_error(function() fenster.rgb(0, 'ERROR', 0) end) 204 | assert.has_error(function() fenster.rgb(0, true, 0) end) 205 | assert.has_error(function() fenster.rgb(0, {}, 0) end) 206 | assert.has_error(function() fenster.rgb(0, function() end, 0) end) 207 | assert.has_error(function() fenster.rgb(0, io.stdout, 0) end) 208 | assert.has_error(function() fenster.rgb(0, 255.5, 0) end) 209 | 210 | assert.has_error(function() fenster.rgb(0, 0, 'ERROR') end) 211 | assert.has_error(function() fenster.rgb(0, 0, true) end) 212 | assert.has_error(function() fenster.rgb(0, 0, {}) end) 213 | assert.has_error(function() fenster.rgb(0, 0, function() end) end) 214 | assert.has_error(function() fenster.rgb(0, 0, io.stdout) end) 215 | assert.has_error(function() fenster.rgb(0, 0, 255.5) end) 216 | end) 217 | 218 | it('should throw when r/g/b are out of range', function() 219 | assert.has_error(function() fenster.rgb(-1, 0, 0) end) 220 | assert.has_error(function() fenster.rgb(256, 0, 0) end) 221 | 222 | assert.has_error(function() fenster.rgb(0, -1, 0) end) 223 | assert.has_error(function() fenster.rgb(0, 256, 0) end) 224 | 225 | assert.has_error(function() fenster.rgb(0, 0, -1) end) 226 | assert.has_error(function() fenster.rgb(0, 0, 256) end) 227 | end) 228 | 229 | it('should convert r/g/b to a color', function() 230 | assert.are_equal(fenster.rgb(0, 0, 0), 0x000000) 231 | assert.are_equal(fenster.rgb(0, 0, 255), 0x0000ff) 232 | assert.are_equal(fenster.rgb(0, 255, 0), 0x00ff00) 233 | assert.are_equal(fenster.rgb(0, 255, 255), 0x00ffff) 234 | assert.are_equal(fenster.rgb(255, 0, 0), 0xff0000) 235 | assert.are_equal(fenster.rgb(255, 0, 255), 0xff00ff) 236 | assert.are_equal(fenster.rgb(255, 255, 0), 0xffff00) 237 | assert.are_equal(fenster.rgb(255, 255, 255), 0xffffff) 238 | assert.are_equal(fenster.rgb(190, 239, 153), 0xbeef99) 239 | end) 240 | end) 241 | 242 | describe('window:close(...) / fenster.close(...)', function() 243 | it('should throw when no arguments were given when not using as method', function() 244 | assert.has_error(function() fenster.close() end) 245 | end) 246 | 247 | it('should throw when window is not a window userdata when not using as method', function() 248 | assert.has_error(function() fenster.close(25) end) 249 | assert.has_error(function() fenster.close(2.5) end) 250 | assert.has_error(function() fenster.close('ERROR') end) 251 | assert.has_error(function() fenster.close(true) end) 252 | assert.has_error(function() fenster.close({}) end) 253 | assert.has_error(function() fenster.close(function() end) end) 254 | assert.has_error(function() fenster.close(io.stdout) end) 255 | end) 256 | 257 | it('should throw when window is used after closing #needsdisplay', function() 258 | local window = fenster.open(256, 144) 259 | fenster.close(window) 260 | assert.has_error(function() fenster.close(window) end) 261 | assert.has_error(function() fenster.loop(window) end) 262 | assert.has_error(function() fenster.set(window, 0, 0, 0xffffff) end) 263 | assert.has_error(function() fenster.get(window, 0, 0) end) 264 | assert.has_error(function() fenster.clear(window) end) 265 | 266 | local window2 = fenster.open(256, 144) 267 | window2:close() 268 | assert.has_error(function() window2:close() end) 269 | assert.has_error(function() window2:loop() end) 270 | assert.has_error(function() window2:set(0, 0, 0xffffff) end) 271 | assert.has_error(function() window2:get(0, 0) end) 272 | assert.has_error(function() window2:clear() end) 273 | end) 274 | end) 275 | 276 | describe('window:loop(...) / fenster.loop(...)', function() 277 | it('should throw when no arguments were given when not using as method', function() 278 | assert.has_error(function() fenster.loop() end) 279 | end) 280 | 281 | it('should throw when window is not a window userdata when not using as method', function() 282 | assert.has_error(function() fenster.loop(25) end) 283 | assert.has_error(function() fenster.loop(2.5) end) 284 | assert.has_error(function() fenster.loop('ERROR') end) 285 | assert.has_error(function() fenster.loop(true) end) 286 | assert.has_error(function() fenster.loop({}) end) 287 | assert.has_error(function() fenster.loop(function() end) end) 288 | assert.has_error(function() fenster.loop(io.stdout) end) 289 | end) 290 | 291 | it('should update the window successfully #needsdisplay', function() 292 | local window = fenster.open(256, 144) 293 | local window2 = fenster.open(256, 144) 294 | finally(function() 295 | fenster.close(window) 296 | window2:close() 297 | end) 298 | assert.is_true(fenster.loop(window)) 299 | assert.is_true(window2:loop()) 300 | end) 301 | end) 302 | 303 | describe('window:set(...) / fenster.set(...)', function() 304 | it('should throw when no arguments were given when not using as method', function() 305 | assert.has_error(function() fenster.set() end) 306 | end) 307 | 308 | it('should throw when window is not a window userdata when not using as method', function() 309 | assert.has_error(function() fenster.set(25, 0, 0, 0) end) 310 | assert.has_error(function() fenster.set(2.5, 0, 0, 0) end) 311 | assert.has_error(function() fenster.set('ERROR', 0, 0, 0) end) 312 | assert.has_error(function() fenster.set(true, 0, 0, 0) end) 313 | assert.has_error(function() fenster.set({}, 0, 0, 0) end) 314 | assert.has_error(function() fenster.set(function() end, 0, 0, 0) end) 315 | assert.has_error(function() fenster.set(io.stdout, 0, 0, 0) end) 316 | end) 317 | 318 | it('should throw when x/y are not integers #needsdisplay', function() 319 | local window = fenster.open(256, 144) 320 | finally(function() window:close() end) 321 | 322 | assert.has_error(function() fenster.set(window, 'ERROR', 0, 0) end) 323 | assert.has_error(function() fenster.set(window, true, 0, 0) end) 324 | assert.has_error(function() fenster.set(window, {}, 0, 0) end) 325 | assert.has_error(function() fenster.set(window, function() end, 0, 0) end) 326 | assert.has_error(function() fenster.set(window, io.stdout, 0, 0) end) 327 | assert.has_error(function() fenster.set(window, 2.5, 0, 0) end) 328 | assert.has_error(function() fenster.set(window, 0, 'ERROR', 0) end) 329 | assert.has_error(function() fenster.set(window, 0, true, 0) end) 330 | assert.has_error(function() fenster.set(window, 0, {}, 0) end) 331 | assert.has_error(function() fenster.set(window, 0, function() end, 0) end) 332 | assert.has_error(function() fenster.set(window, 0, io.stdout, 0) end) 333 | assert.has_error(function() fenster.set(window, 0, 2.5, 0) end) 334 | 335 | assert.has_error(function() window:set('ERROR', 0, 0) end) 336 | assert.has_error(function() window:set(true, 0, 0) end) 337 | assert.has_error(function() window:set({}, 0, 0) end) 338 | assert.has_error(function() window:set(function() end, 0, 0) end) 339 | assert.has_error(function() window:set(io.stdout, 0, 0) end) 340 | assert.has_error(function() window:set(2.5, 0, 0) end) 341 | assert.has_error(function() window:set(0, 'ERROR', 0) end) 342 | assert.has_error(function() window:set(0, true, 0) end) 343 | assert.has_error(function() window:set(0, {}, 0) end) 344 | assert.has_error(function() window:set(0, function() end, 0) end) 345 | assert.has_error(function() window:set(0, io.stdout, 0) end) 346 | assert.has_error(function() window:set(0, 2.5, 0) end) 347 | end) 348 | 349 | it('should throw when color is not an integer #needsdisplay', function() 350 | local window = fenster.open(256, 144) 351 | finally(function() window:close() end) 352 | 353 | assert.has_error(function() fenster.set(window, 0, 0, 'ERROR') end) 354 | assert.has_error(function() fenster.set(window, 0, 0, true) end) 355 | assert.has_error(function() fenster.set(window, 0, 0, {}) end) 356 | assert.has_error(function() fenster.set(window, 0, 0, function() end) end) 357 | assert.has_error(function() fenster.set(window, 0, 0, io.stdout) end) 358 | assert.has_error(function() fenster.set(window, 0, 0, 2.5) end) 359 | 360 | assert.has_error(function() window:set(0, 0, 'ERROR') end) 361 | assert.has_error(function() window:set(0, 0, true) end) 362 | assert.has_error(function() window:set(0, 0, {}) end) 363 | assert.has_error(function() window:set(0, 0, function() end) end) 364 | assert.has_error(function() window:set(0, 0, io.stdout) end) 365 | assert.has_error(function() window:set(0, 0, 2.5) end) 366 | end) 367 | 368 | it('should throw when x/y are out of range #needsdisplay', function() 369 | local window = fenster.open(256, 144) 370 | finally(function() window:close() end) 371 | 372 | assert.has_error(function() fenster.set(window, -1, 0, 0) end) 373 | assert.has_error(function() fenster.set(window, 256, 0, 0) end) 374 | assert.has_error(function() fenster.set(window, 0, -1, 0) end) 375 | assert.has_error(function() fenster.set(window, 0, 144, 0) end) 376 | 377 | assert.has_error(function() window:set(-1, 0, 0) end) 378 | assert.has_error(function() window:set(256, 0, 0) end) 379 | assert.has_error(function() window:set(0, -1, 0) end) 380 | assert.has_error(function() window:set(0, 144, 0) end) 381 | end) 382 | 383 | it('should throw when color is out of range #needsdisplay', function() 384 | local window = fenster.open(256, 144) 385 | finally(function() window:close() end) 386 | 387 | assert.has_error(function() fenster.set(window, 0, 0, -1) end) 388 | assert.has_error(function() fenster.set(window, 0, 0, 0x1000000) end) 389 | 390 | assert.has_error(function() window:set(0, 0, -1) end) 391 | assert.has_error(function() window:set(0, 0, 0x1000000) end) 392 | end) 393 | 394 | it('should set a pixel successfully #needsdisplay', function() 395 | local window = fenster.open(256, 144) 396 | finally(function() window:close() end) 397 | 398 | fenster.set(window, 20, 20, 0xbeef99) 399 | assert.are_equal(fenster.get(window, 20, 20), 0xbeef99) 400 | 401 | window:set(20, 20, 0xbeef99) 402 | assert.are_equal(window:get(20, 20), 0xbeef99) 403 | end) 404 | end) 405 | 406 | describe('window:get(...) / fenster.get(...)', function() 407 | it('should throw when no arguments were given when not using as method', function() 408 | assert.has_error(function() fenster.get() end) 409 | end) 410 | 411 | it('should throw when window is not a window userdata when not using as method', function() 412 | assert.has_error(function() fenster.get(25, 0, 0) end) 413 | assert.has_error(function() fenster.get(2.5, 0, 0) end) 414 | assert.has_error(function() fenster.get('ERROR', 0, 0) end) 415 | assert.has_error(function() fenster.get(true, 0, 0) end) 416 | assert.has_error(function() fenster.get({}, 0, 0) end) 417 | assert.has_error(function() fenster.get(function() end, 0, 0) end) 418 | assert.has_error(function() fenster.get(io.stdout, 0, 0) end) 419 | end) 420 | 421 | it('should throw when x/y are not integers #needsdisplay', function() 422 | local window = fenster.open(256, 144) 423 | finally(function() window:close() end) 424 | 425 | assert.has_error(function() fenster.get(window, 'ERROR', 0) end) 426 | assert.has_error(function() fenster.get(window, true, 0) end) 427 | assert.has_error(function() fenster.get(window, {}, 0) end) 428 | assert.has_error(function() fenster.get(window, function() end, 0) end) 429 | assert.has_error(function() fenster.get(window, io.stdout, 0) end) 430 | assert.has_error(function() fenster.get(window, 2.5, 0) end) 431 | assert.has_error(function() fenster.get(window, 0, 'ERROR') end) 432 | assert.has_error(function() fenster.get(window, 0, true) end) 433 | assert.has_error(function() fenster.get(window, 0, {}) end) 434 | assert.has_error(function() fenster.get(window, 0, function() end) end) 435 | assert.has_error(function() fenster.get(window, 0, io.stdout) end) 436 | assert.has_error(function() fenster.get(window, 0, 2.5) end) 437 | 438 | assert.has_error(function() window:get('ERROR', 0) end) 439 | assert.has_error(function() window:get(true, 0) end) 440 | assert.has_error(function() window:get({}, 0) end) 441 | assert.has_error(function() window:get(function() end, 0) end) 442 | assert.has_error(function() window:get(io.stdout, 0) end) 443 | assert.has_error(function() window:get(2.5, 0) end) 444 | assert.has_error(function() window:get(0, 'ERROR') end) 445 | assert.has_error(function() window:get(0, true) end) 446 | assert.has_error(function() window:get(0, {}) end) 447 | assert.has_error(function() window:get(0, function() end) end) 448 | assert.has_error(function() window:get(0, io.stdout) end) 449 | assert.has_error(function() window:get(0, 2.5) end) 450 | end) 451 | 452 | it('should throw when x/y are out of range #needsdisplay', function() 453 | local window = fenster.open(256, 144) 454 | finally(function() window:close() end) 455 | 456 | assert.has_error(function() fenster.get(window, -1, 0) end) 457 | assert.has_error(function() fenster.get(window, 256, 0) end) 458 | assert.has_error(function() fenster.get(window, 0, -1) end) 459 | assert.has_error(function() fenster.get(window, 0, 144) end) 460 | 461 | assert.has_error(function() window:get(-1, 0) end) 462 | assert.has_error(function() window:get(256, 0) end) 463 | assert.has_error(function() window:get(0, -1) end) 464 | assert.has_error(function() window:get(0, 144) end) 465 | end) 466 | 467 | it('should return the color of a pixel successfully #needsdisplay', function() 468 | local window = fenster.open(256, 144) 469 | finally(function() window:close() end) 470 | 471 | fenster.set(window, 0, 0, 0xf00f00) 472 | assert.are_equal(fenster.get(window, 0, 0), 0xf00f00) 473 | 474 | window:set(0, 0, 0xf00f00) 475 | assert.are_equal(window:get(0, 0), 0xf00f00) 476 | end) 477 | end) 478 | 479 | describe('window:clear(...) / fenster.clear(...)', function() 480 | it('should throw when no arguments were given when not using as method', function() 481 | assert.has_error(function() fenster.clear() end) 482 | end) 483 | 484 | it('should throw when window is not a window userdata when not using as method', function() 485 | assert.has_error(function() fenster.clear(25) end) 486 | assert.has_error(function() fenster.clear(2.5) end) 487 | assert.has_error(function() fenster.clear('ERROR') end) 488 | assert.has_error(function() fenster.clear(true) end) 489 | assert.has_error(function() fenster.clear({}) end) 490 | assert.has_error(function() fenster.clear(function() end) end) 491 | assert.has_error(function() fenster.clear(io.stdout) end) 492 | end) 493 | 494 | it('should clear the window successfully #needsdisplay', function() 495 | local window = fenster.open(256, 144) 496 | finally(function() window:close() end) 497 | 498 | fenster.set(window, 0, 0, 0xf00f00) 499 | fenster.set(window, 255, 143, 0xbeef99) 500 | fenster.clear(window) 501 | assert.are_equal(fenster.get(window, 0, 0), 0x000000) 502 | assert.are_equal(fenster.get(window, 255, 143), 0x000000) 503 | 504 | window:set(0, 0, 0xf00f00) 505 | window:set(255, 143, 0xbeef99) 506 | window:clear() 507 | assert.are_equal(window:get(0, 0), 0x000000) 508 | assert.are_equal(window:get(255, 143), 0x000000) 509 | end) 510 | 511 | it('should throw when color is not an integer #needsdisplay', function() 512 | local window = fenster.open(256, 144) 513 | finally(function() window:close() end) 514 | 515 | assert.has_error(function() fenster.clear(window, 'ERROR') end) 516 | assert.has_error(function() fenster.clear(window, true) end) 517 | assert.has_error(function() fenster.clear(window, {}) end) 518 | assert.has_error(function() fenster.clear(window, function() end) end) 519 | assert.has_error(function() fenster.clear(window, io.stdout) end) 520 | assert.has_error(function() fenster.clear(window, 2.5) end) 521 | 522 | assert.has_error(function() window:clear('ERROR') end) 523 | assert.has_error(function() window:clear(true) end) 524 | assert.has_error(function() window:clear({}) end) 525 | assert.has_error(function() window:clear(function() end) end) 526 | assert.has_error(function() window:clear(io.stdout) end) 527 | assert.has_error(function() window:clear(2.5) end) 528 | end) 529 | 530 | it('should throw when color is out of range #needsdisplay', function() 531 | local window = fenster.open(256, 144) 532 | finally(function() window:close() end) 533 | 534 | assert.has_error(function() fenster.clear(window, -1) end) 535 | assert.has_error(function() fenster.clear(window, 0x1000000) end) 536 | 537 | assert.has_error(function() window:clear(-1) end) 538 | assert.has_error(function() window:clear(0x1000000) end) 539 | end) 540 | 541 | it('should clear the window with a color successfully #needsdisplay', function() 542 | local window = fenster.open(256, 144) 543 | finally(function() window:close() end) 544 | 545 | fenster.set(window, 0, 0, 0xf00f00) 546 | fenster.set(window, 255, 143, 0xbeef99) 547 | fenster.clear(window, 0x00ff00) 548 | assert.are_equal(fenster.get(window, 0, 0), 0x00ff00) 549 | assert.are_equal(fenster.get(window, 255, 143), 0x00ff00) 550 | 551 | window:set(0, 0, 0xf00f00) 552 | window:set(255, 143, 0xbeef99) 553 | window:clear(0xff0000) 554 | assert.are_equal(window:get(0, 0), 0xff0000) 555 | assert.are_equal(window:get(255, 143), 0xff0000) 556 | end) 557 | end) 558 | 559 | describe('window.keys', function() 560 | it('should be a table of 256 booleans #needsdisplay', function() 561 | local window = fenster.open(256, 144) 562 | finally(function() window:close() end) 563 | assert.is_table(window.keys) 564 | 565 | local index = 0 566 | while window.keys[index] ~= nil do 567 | assert.is_boolean(window.keys[index]) 568 | assert.is_false(window.keys[index]) 569 | index = index + 1 ---@type integer 570 | end 571 | assert.are_equal(index, 256) 572 | end) 573 | end) 574 | 575 | describe('window.delta', function() 576 | it('should be a number and be zero before the second frame #needsdisplay', function() 577 | local window = fenster.open(256, 144) 578 | finally(function() window:close() end) 579 | assert.is_number(window.delta) 580 | assert.are_equal(window.delta, 0) 581 | window:loop() 582 | assert.are_equal(window.delta, 0) 583 | end) 584 | end) 585 | 586 | describe('window.mousex', function() 587 | it('should be a integer and be zero before the first frame #needsdisplay', function() 588 | local window = fenster.open(256, 144) 589 | finally(function() window:close() end) 590 | assert.is_number(window.mousex) 591 | assert.are_equal(window.mousex, 0) 592 | end) 593 | end) 594 | 595 | describe('window.mousey', function() 596 | it('should be a integer and be zero before the first frame #needsdisplay', function() 597 | local window = fenster.open(256, 144) 598 | finally(function() window:close() end) 599 | assert.is_number(window.mousey) 600 | assert.are_equal(window.mousey, 0) 601 | end) 602 | end) 603 | 604 | describe('window.mousedown', function() 605 | it('should be a boolean and be false before the first frame #needsdisplay', function() 606 | local window = fenster.open(256, 144) 607 | finally(function() window:close() end) 608 | assert.is_boolean(window.mousedown) 609 | assert.is_false(window.mousedown) 610 | end) 611 | end) 612 | 613 | describe('window.modcontrol', function() 614 | it('should be a boolean and be false before the first frame #needsdisplay', function() 615 | local window = fenster.open(256, 144) 616 | finally(function() window:close() end) 617 | assert.is_boolean(window.modcontrol) 618 | assert.is_false(window.modcontrol) 619 | end) 620 | end) 621 | 622 | describe('window.modshift', function() 623 | it('should be a boolean and be false before the first frame #needsdisplay', function() 624 | local window = fenster.open(256, 144) 625 | finally(function() window:close() end) 626 | assert.is_boolean(window.modshift) 627 | assert.is_false(window.modshift) 628 | end) 629 | end) 630 | 631 | describe('window.modalt', function() 632 | it('should be a boolean and be false before the first frame #needsdisplay', function() 633 | local window = fenster.open(256, 144) 634 | finally(function() window:close() end) 635 | assert.is_boolean(window.modalt) 636 | assert.is_false(window.modalt) 637 | end) 638 | end) 639 | 640 | describe('window.modgui', function() 641 | it('should be a boolean and be false before the first frame #needsdisplay', function() 642 | local window = fenster.open(256, 144) 643 | finally(function() window:close() end) 644 | assert.is_boolean(window.modgui) 645 | assert.is_false(window.modgui) 646 | end) 647 | end) 648 | 649 | describe('window.width', function() 650 | it('should be an integer and be the same as passed to open #needsdisplay', function() 651 | local window = fenster.open(256, 144) 652 | finally(function() window:close() end) 653 | assert.is_number(window.width) 654 | assert.are_equal(window.width, 256) 655 | end) 656 | end) 657 | 658 | describe('window.height', function() 659 | it('should be an integer and be the same as passed to open #needsdisplay', function() 660 | local window = fenster.open(256, 144) 661 | finally(function() window:close() end) 662 | assert.is_number(window.height) 663 | assert.are_equal(window.height, 144) 664 | end) 665 | end) 666 | 667 | describe('window.title', function() 668 | it('should be a string and be the same as passed to open #needsdisplay', function() 669 | local window = fenster.open(256, 144, 'Test') 670 | finally(function() window:close() end) 671 | assert.is_string(window.title) 672 | assert.are_equal(window.title, 'Test') 673 | end) 674 | end) 675 | 676 | describe('window.scale', function() 677 | it('should be an integer and be the same as passed to open #needsdisplay', function() 678 | local window = fenster.open(256, 144, 'Test', 4) 679 | finally(function() window:close() end) 680 | assert.is_number(window.scale) 681 | assert.are_equal(window.scale, 4) 682 | end) 683 | end) 684 | 685 | describe('window.targetfps', function() 686 | it('should be an integer and be the same as passed to open #needsdisplay', function() 687 | local window = fenster.open(256, 144, 'Test', 1, 30) 688 | finally(function() window:close() end) 689 | assert.is_number(window.targetfps) 690 | assert.are_equal(window.targetfps, 30) 691 | end) 692 | end) 693 | end) 694 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include "../include/main.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "../lib/compat-5.3/compat-5.3.h" 12 | #include "../lib/fenster/fenster.h" 13 | 14 | // Macros that ensure the same integer argument behavior in Lua 5.1/5.2 15 | // and 5.3/5.4. In Lua 5.1/5.2 luaL_checkinteger/luaL_optinteger normally floor 16 | // decimal numbers, while in Lua 5.3/5.4 they throw an error. These macros make 17 | // sure to always throw an error if the number has a decimal part. 18 | #if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM <= 502 19 | 20 | #define luaL_checkinteger(L, narg) \ 21 | (luaL_argcheck(L, lua_tointeger(L, narg) == lua_tonumber(L, narg), narg, \ 22 | "number has no integer representation"), \ 23 | luaL_checkinteger(L, narg)) 24 | 25 | #define luaL_optinteger(L, narg, def) luaL_opt(L, luaL_checkinteger, narg, def) 26 | 27 | #endif 28 | 29 | /** Default window title */ 30 | static const char *DEFAULT_TITLE = "fenster"; 31 | 32 | /** Default window scale */ 33 | static const lua_Integer DEFAULT_SCALE = 1; 34 | 35 | /** Default target frames per second */ 36 | static const lua_Number DEFAULT_TARGET_FPS = 60.0; 37 | 38 | /** Number of milliseconds per second */ 39 | static const lua_Number MS_PER_SEC = 1000.0; 40 | 41 | /** Length of the fenster->keys array */ 42 | static const int KEYS_LENGTH = sizeof(((struct fenster *)0)->keys) / 43 | sizeof(((struct fenster *)0)->keys[0]); 44 | 45 | /** Maximum width/height of the window */ 46 | static const lua_Integer MAX_DIMENSION = 15360; 47 | 48 | /** Maximum color value */ 49 | static const lua_Integer MAX_COLOR = 0xffffff; 50 | 51 | /** Maximum value of a color component (r, g or b) */ 52 | static const lua_Integer MAX_COLOR_COMPONENT = 0xff; 53 | 54 | /** Bit offset of the red color component in a color value */ 55 | static const lua_Integer COLOR_RED_OFFSET = 16; 56 | 57 | /** Bit offset of the green color component in a color value */ 58 | static const lua_Integer COLOR_GREEN_OFFSET = 8; 59 | 60 | /** Name of the window userdata and metatable */ 61 | static const char *WINDOW_METATABLE = "window*"; 62 | 63 | /** Userdata representing the fenster window */ 64 | typedef struct window { 65 | // "private" members 66 | struct fenster *p_fenster; 67 | int keys_ref; 68 | int64_t target_frame_time; 69 | int64_t start_frame_time; 70 | size_t scaled_pixels; 71 | 72 | // "public" members 73 | lua_Number delta; 74 | lua_Integer scaled_mouse_x; 75 | lua_Integer scaled_mouse_y; 76 | int mod_control; 77 | int mod_shift; 78 | int mod_alt; 79 | int mod_gui; 80 | lua_Integer width; 81 | lua_Integer height; 82 | lua_Integer scale; 83 | lua_Number target_fps; 84 | } window; 85 | 86 | /* 87 | // Utility function to dump the Lua stack for debugging 88 | static void _dumpstack(lua_State *L) { 89 | int top = lua_gettop(L); 90 | for (int i = 1; i <= top; i++) { 91 | printf("%d\t%s\t", i, luaL_typename(L, i)); 92 | switch (lua_type(L, i)) { 93 | case LUA_TNUMBER:printf("%g\n", lua_tonumber(L, i)); 94 | break; 95 | case LUA_TSTRING:printf("%s\n", lua_tostring(L, i)); 96 | break; 97 | case LUA_TBOOLEAN:printf("%s\n", lua_toboolean(L, i) ? "true" : "false"); 98 | break; 99 | case LUA_TNIL:printf("%s\n", "nil"); 100 | break; 101 | default:printf("%p\n", lua_topointer(L, i)); 102 | break; 103 | } 104 | } 105 | } 106 | */ 107 | 108 | /** 109 | * Utility function to get a dimension value from the Lua stack and check if 110 | * it's within the allowed range. 111 | * @param L Lua state 112 | * @param index Index of the dimension value on the Lua stack 113 | * @return The dimension value 114 | */ 115 | static lua_Integer check_dimension(lua_State *L, int index) { 116 | const lua_Integer dimension = luaL_checkinteger(L, index); 117 | luaL_argcheck(L, dimension > 0 && dimension <= MAX_DIMENSION, index, 118 | "width/height must be in range 1-15360"); 119 | return dimension; 120 | } 121 | 122 | /** 123 | * Opens a window with the given width, height, title, scale and target FPS. 124 | * Returns a userdata representing the window with all the methods and 125 | * properties we defined on the metatable. 126 | * @param L Lua state 127 | * @return Number of return values on the Lua stack 128 | */ 129 | static int lfenster_open(lua_State *L) { 130 | const lua_Integer width = check_dimension(L, 1); 131 | const lua_Integer height = check_dimension(L, 2); 132 | const char *title = luaL_optstring(L, 3, DEFAULT_TITLE); 133 | const lua_Integer scale = luaL_optinteger(L, 4, DEFAULT_SCALE); 134 | luaL_argcheck(L, (scale & (scale - 1)) == 0, 4, "scale must be a power of 2"); 135 | const lua_Number target_fps = luaL_optnumber(L, 5, DEFAULT_TARGET_FPS); 136 | luaL_argcheck(L, target_fps >= 0.0, 5, "target fps must be non-negative"); 137 | 138 | // calculate the scaled width, scaled height and amount of pixels 139 | const size_t scaled_width = width * scale; 140 | const size_t scaled_height = height * scale; 141 | const size_t scaled_pixels = scaled_width * scaled_height; 142 | 143 | // allocate memory for the window buffer 144 | uint32_t *buffer = calloc(scaled_pixels, sizeof(uint32_t)); 145 | if (buffer == NULL) { 146 | const int error = errno; 147 | return luaL_error( 148 | L, "failed to allocate memory of size %d for window buffer (%d)", 149 | scaled_pixels * sizeof(uint32_t), error); 150 | } 151 | 152 | // use a temporary fenster struct to copy into the "real" one later 153 | // (width and height use narrow casts to int, but we made sure it's in range) 154 | struct fenster temp_fenster = { 155 | .title = title, 156 | .width = (int)scaled_width, 157 | .height = (int)scaled_height, 158 | .buf = buffer, 159 | }; 160 | 161 | // allocate memory for the "real" fenster struct 162 | struct fenster *p_fenster = malloc(sizeof(struct fenster)); 163 | if (p_fenster == NULL) { 164 | const int error = errno; 165 | free(buffer); 166 | buffer = NULL; 167 | return luaL_error(L, "failed to allocate memory of size %d for window (%d)", 168 | sizeof(struct fenster), error); 169 | } 170 | 171 | // copy temporary fenster struct into the "real" one 172 | // (we have to do it this way because width and height are const) 173 | memcpy(p_fenster, &temp_fenster, sizeof(struct fenster)); 174 | 175 | // open window and check success 176 | const int result = fenster_open(p_fenster); 177 | if (result != 0) { 178 | free(buffer); 179 | buffer = NULL; 180 | free(p_fenster); 181 | p_fenster = NULL; 182 | return luaL_error(L, "failed to open window (%d)", result); 183 | } 184 | 185 | // initialize the keys table and put it in the registry 186 | lua_createtable(L, KEYS_LENGTH, 0); 187 | for (int i = 0; i < KEYS_LENGTH; i++) { 188 | lua_pushboolean(L, 0); 189 | lua_rawseti(L, -2, i); 190 | } 191 | lua_pushvalue(L, -1); // copy the keys table since luaL_ref pops it 192 | const int keys_ref = luaL_ref(L, LUA_REGISTRYINDEX); 193 | if (keys_ref == LUA_REFNIL || keys_ref == LUA_NOREF) { 194 | fenster_close(p_fenster); 195 | free(buffer); 196 | buffer = NULL; 197 | free(p_fenster); 198 | p_fenster = NULL; 199 | return luaL_error(L, "failed to create keys table (%d)", keys_ref); 200 | } 201 | lua_rawseti(L, LUA_REGISTRYINDEX, keys_ref); 202 | 203 | // create the window userdata and initialize it 204 | window *p_window = lua_newuserdata(L, sizeof(window)); 205 | p_window->p_fenster = p_fenster; 206 | p_window->keys_ref = keys_ref; 207 | p_window->target_frame_time = 208 | target_fps ? llroundl(MS_PER_SEC / target_fps) : 0; 209 | p_window->start_frame_time = 0; 210 | p_window->scaled_pixels = scaled_pixels; 211 | p_window->delta = 0.0; 212 | p_window->scaled_mouse_x = 0; 213 | p_window->scaled_mouse_y = 0; 214 | p_window->mod_control = 0; 215 | p_window->mod_shift = 0; 216 | p_window->mod_alt = 0; 217 | p_window->mod_gui = 0; 218 | p_window->width = width; 219 | p_window->height = height; 220 | p_window->scale = scale; 221 | p_window->target_fps = target_fps; 222 | luaL_setmetatable(L, WINDOW_METATABLE); 223 | return 1; 224 | } 225 | 226 | /** 227 | * Pauses for a given number of milliseconds. 228 | * @param L Lua state 229 | * @return Number of return values on the Lua stack 230 | */ 231 | static int lfenster_sleep(lua_State *L) { 232 | const lua_Integer milliseconds = luaL_checkinteger(L, 1); 233 | 234 | fenster_sleep(milliseconds); 235 | 236 | return 0; 237 | } 238 | 239 | /** 240 | * Returns the current time in milliseconds. 241 | * @param L Lua state 242 | * @return Number of return values on the Lua stack 243 | */ 244 | static int lfenster_time(lua_State *L) { 245 | lua_pushinteger(L, fenster_time()); 246 | return 1; 247 | } 248 | 249 | /** 250 | * Utility function to get a color value from the Lua stack and check if it's 251 | * within the allowed range. 252 | * @param L Lua state 253 | * @param index Index of the color value on the Lua stack 254 | * @return The color value 255 | */ 256 | static lua_Integer check_color(lua_State *L, int index) { 257 | const lua_Integer color = luaL_checkinteger(L, index); 258 | luaL_argcheck(L, color >= 0 && color <= MAX_COLOR, index, 259 | "color must be in range 0x000000-0xffffff"); 260 | return color; 261 | } 262 | 263 | /** 264 | * Utility function to get a color component from the Lua stack and check if 265 | * it's within the allowed range. 266 | * @param L Lua state 267 | * @param index Index of the color component on the Lua stack 268 | * @return The color component value 269 | */ 270 | static lua_Integer check_color_component(lua_State *L, int index) { 271 | const lua_Integer color_component = luaL_checkinteger(L, index); 272 | luaL_argcheck(L, 273 | color_component >= 0 && color_component <= MAX_COLOR_COMPONENT, 274 | index, "color component must be in range 0-255"); 275 | return color_component; 276 | } 277 | 278 | /** 279 | * Utility function to convert a color value to RGB components or vice versa. 280 | * @param L Lua state 281 | * @return Number of return values on the Lua stack 282 | */ 283 | static int lfenster_rgb(lua_State *L) { 284 | // check if the function was called with less than 3 arguments 285 | if (lua_gettop(L) < 3) { 286 | // get color value argument 287 | const lua_Integer color = check_color(L, 1); 288 | 289 | // return RGB components 290 | lua_pushinteger(L, (color >> COLOR_RED_OFFSET) & MAX_COLOR_COMPONENT); 291 | lua_pushinteger(L, (color >> COLOR_GREEN_OFFSET) & MAX_COLOR_COMPONENT); 292 | lua_pushinteger(L, color & MAX_COLOR_COMPONENT); 293 | return 3; 294 | } 295 | 296 | // get RGB component arguments 297 | const lua_Integer red = check_color_component(L, 1); 298 | const lua_Integer green = check_color_component(L, 2); 299 | const lua_Integer blue = check_color_component(L, 3); 300 | 301 | // return color value 302 | lua_pushinteger( 303 | L, (red << COLOR_RED_OFFSET) | (green << COLOR_GREEN_OFFSET) | blue); 304 | return 1; 305 | } 306 | 307 | /** Macro to get the window userdata from the Lua stack */ 308 | #define check_window(L) (luaL_checkudata(L, 1, WINDOW_METATABLE)) 309 | 310 | /** Macro to check if the window is closed */ 311 | #define is_window_closed(p_window) ((p_window)->p_fenster == NULL) 312 | 313 | /** 314 | * Utility function to get the window userdata from the Lua stack and check if 315 | * the window is open. 316 | * @param L Lua state 317 | * @return The window userdata 318 | */ 319 | static window *check_open_window(lua_State *L) { 320 | window *p_window = check_window(L); 321 | if (is_window_closed(p_window)) { 322 | luaL_error(L, "attempt to use a closed window"); 323 | } 324 | return p_window; 325 | } 326 | 327 | /** 328 | * Close the window. Does nothing if the window is already closed. 329 | * The __gc and __close meta methods also call this function, so the user 330 | * likely won't need to call this function manually. 331 | * @param L Lua state 332 | * @return Number of return values on the Lua stack 333 | */ 334 | static int window_close(lua_State *L) { 335 | window *p_window = check_open_window(L); 336 | 337 | // close and free window 338 | fenster_close(p_window->p_fenster); 339 | free(p_window->p_fenster->buf); 340 | p_window->p_fenster->buf = NULL; 341 | free(p_window->p_fenster); 342 | p_window->p_fenster = NULL; 343 | luaL_unref(L, LUA_REGISTRYINDEX, p_window->keys_ref); // free keys table 344 | p_window->keys_ref = LUA_NOREF; 345 | 346 | return 0; 347 | } 348 | 349 | /** 350 | * Main loop for the window. Handles FPS limiting and updates delta time, keys, 351 | * mouse coordinates, modifier keys and the whole screen. Returns true if the 352 | * window is still open and false if it's closed (only on Windows right now). 353 | * @param L Lua state 354 | * @return Number of return values on the Lua stack 355 | */ 356 | static int window_loop(lua_State *L) { 357 | window *p_window = check_open_window(L); 358 | 359 | // handle fps limiting 360 | int64_t now = fenster_time(); 361 | if (p_window->start_frame_time == 0) { 362 | // initialize start frame time (this is the first frame) 363 | p_window->start_frame_time = now; 364 | } else { 365 | const int64_t last_frame_time = now - p_window->start_frame_time; 366 | if (p_window->target_frame_time > last_frame_time) { 367 | // sleep for the remaining frame time to reach target frame time 368 | fenster_sleep(p_window->target_frame_time - last_frame_time); 369 | } 370 | now = fenster_time(); // update timestamp after sleeping 371 | p_window->delta = 372 | (lua_Number)(now - p_window->start_frame_time) / MS_PER_SEC; 373 | p_window->start_frame_time = now; 374 | } 375 | 376 | if (fenster_loop(p_window->p_fenster) == 0) { 377 | // update the keys table in the registry 378 | lua_rawgeti(L, LUA_REGISTRYINDEX, p_window->keys_ref); 379 | for (int i = 0; i < KEYS_LENGTH; i++) { 380 | lua_pushboolean(L, p_window->p_fenster->keys[i]); 381 | lua_rawseti(L, -2, i); 382 | } 383 | 384 | // update the scaled mouse coordinates (floors the coordinates) 385 | p_window->scaled_mouse_x = p_window->p_fenster->x / p_window->scale; 386 | p_window->scaled_mouse_y = p_window->p_fenster->y / p_window->scale; 387 | 388 | // update the modifier keys 389 | p_window->mod_control = p_window->p_fenster->mod & 1; 390 | p_window->mod_shift = (p_window->p_fenster->mod >> 1) & 1; 391 | p_window->mod_alt = (p_window->p_fenster->mod >> 2) & 1; 392 | p_window->mod_gui = (p_window->p_fenster->mod >> 3) & 1; 393 | 394 | lua_pushboolean(L, 1); 395 | } else { 396 | lua_pushboolean(L, 0); 397 | } 398 | return 1; 399 | } 400 | 401 | /** 402 | * Utility function to get the x coordinate from the Lua stack and check if it's 403 | * within bounds. 404 | * @param L Lua state 405 | * @param p_window The window userdata 406 | * @return The x coordinate 407 | */ 408 | static lua_Integer check_x(lua_State *L, window *p_window) { 409 | const lua_Integer x = luaL_checkinteger(L, 2); 410 | luaL_argcheck(L, x >= 0 && x < p_window->width, 2, 411 | "x coordinate must be in range 0-[width-1]"); 412 | return x; 413 | } 414 | 415 | /** 416 | * Utility function to get the y coordinate from the Lua stack and check if it's 417 | * within bounds. 418 | * @param L Lua state 419 | * @param p_window The window userdata 420 | * @return The y coordinate 421 | */ 422 | static lua_Integer check_y(lua_State *L, window *p_window) { 423 | const lua_Integer y = luaL_checkinteger(L, 3); 424 | luaL_argcheck(L, y >= 0 && y < p_window->height, 3, 425 | "y coordinate must be in range 0-[height-1]"); 426 | return y; 427 | } 428 | 429 | /** 430 | * Set a pixel in the window buffer at the given coordinates to the given color. 431 | * @param L Lua state 432 | * @return Number of return values on the Lua stack 433 | */ 434 | static int window_set(lua_State *L) { 435 | window *p_window = check_open_window(L); 436 | const lua_Integer x = check_x(L, p_window); 437 | const lua_Integer y = check_y(L, p_window); 438 | const lua_Integer color = check_color(L, 4); 439 | 440 | // set the pixel at the scaled coordinates to the given color 441 | // (repeat this for each copy of the pixel in an area the size of the scale) 442 | lua_Integer scaled_y = y * p_window->scale; 443 | const lua_Integer scaled_y_end = scaled_y + p_window->scale; 444 | lua_Integer scaled_x; 445 | const lua_Integer scaled_x_begin = x * p_window->scale; 446 | const lua_Integer scaled_x_end = scaled_x_begin + p_window->scale; 447 | for (; scaled_y < scaled_y_end; scaled_y++) { 448 | for (scaled_x = scaled_x_begin; scaled_x < scaled_x_end; scaled_x++) { 449 | fenster_pixel(p_window->p_fenster, scaled_x, scaled_y) = color; 450 | } 451 | } 452 | 453 | return 0; 454 | } 455 | 456 | /** 457 | * Get the color of a pixel in the window buffer at the given coordinates. 458 | * @param L Lua state 459 | * @return Number of return values on the Lua stack 460 | */ 461 | static int window_get(lua_State *L) { 462 | window *p_window = check_open_window(L); 463 | const lua_Integer x = check_x(L, p_window); 464 | const lua_Integer y = check_y(L, p_window); 465 | 466 | // get the color of the pixel at the scaled coordinates 467 | // (we don't need a loop here like in the set method because we only need 468 | // the color of the first pixel in the scaled area - they should all be same) 469 | lua_pushinteger(L, fenster_pixel(p_window->p_fenster, x * p_window->scale, 470 | y * p_window->scale)); 471 | return 1; 472 | } 473 | 474 | /** 475 | * Clear the window buffer with the given color. 476 | * @param L Lua state 477 | * @return Number of return values on the Lua stack 478 | */ 479 | static int window_clear(lua_State *L) { 480 | window *p_window = check_open_window(L); 481 | // we cant use check_color here since it's optional... 482 | const lua_Integer color = luaL_optinteger(L, 2, 0x000000); 483 | luaL_argcheck(L, color >= 0 && color <= MAX_COLOR, 2, 484 | "color must be in range 0x000000-0xffffff"); 485 | 486 | // overwrite the whole buffer with the given color 487 | for (size_t i = 0; i < p_window->scaled_pixels; i++) { 488 | p_window->p_fenster->buf[i] = color; 489 | } 490 | 491 | return 0; 492 | } 493 | 494 | /** 495 | * Index function for the window userdata. Checks if the key exists in the 496 | * methods metatable and returns the method if it does. Otherwise, checks for 497 | * properties and returns the property value if it exists. 498 | * @param L Lua state 499 | * @return Number of return values on the Lua stack 500 | */ 501 | static int window_index(lua_State *L) { 502 | window *p_window = check_open_window(L); 503 | const char *key = luaL_checkstring(L, 2); 504 | 505 | // check if the key exists in the methods metatable 506 | luaL_getmetatable(L, WINDOW_METATABLE); 507 | lua_pushvalue(L, 2); 508 | lua_rawget(L, -2); 509 | if (lua_isnil(L, -1)) { 510 | // key not found in the methods metatable, check for properties 511 | if (strcmp(key, "keys") == 0) { 512 | // retrieve the keys table from the registry 513 | lua_rawgeti(L, LUA_REGISTRYINDEX, p_window->keys_ref); 514 | } else if (strcmp(key, "delta") == 0) { 515 | lua_pushnumber(L, p_window->delta); 516 | } else if (strcmp(key, "mousex") == 0) { 517 | lua_pushinteger(L, p_window->scaled_mouse_x); 518 | } else if (strcmp(key, "mousey") == 0) { 519 | lua_pushinteger(L, p_window->scaled_mouse_y); 520 | } else if (strcmp(key, "mousedown") == 0) { 521 | lua_pushboolean(L, p_window->p_fenster->mouse); 522 | } else if (strcmp(key, "modcontrol") == 0) { 523 | lua_pushboolean(L, p_window->mod_control); 524 | } else if (strcmp(key, "modshift") == 0) { 525 | lua_pushboolean(L, p_window->mod_shift); 526 | } else if (strcmp(key, "modalt") == 0) { 527 | lua_pushboolean(L, p_window->mod_alt); 528 | } else if (strcmp(key, "modgui") == 0) { 529 | lua_pushboolean(L, p_window->mod_gui); 530 | } else if (strcmp(key, "width") == 0) { 531 | lua_pushinteger(L, p_window->width); 532 | } else if (strcmp(key, "height") == 0) { 533 | lua_pushinteger(L, p_window->height); 534 | } else if (strcmp(key, "title") == 0) { 535 | lua_pushstring(L, p_window->p_fenster->title); 536 | } else if (strcmp(key, "scale") == 0) { 537 | lua_pushinteger(L, p_window->scale); 538 | } else if (strcmp(key, "targetfps") == 0) { 539 | lua_pushnumber(L, p_window->target_fps); 540 | } else { 541 | // no matching key is found, return nil 542 | lua_pushnil(L); 543 | } 544 | } 545 | return 1; // return either the method or the property value 546 | } 547 | 548 | /** 549 | * Close the window when the window userdata is garbage collected. 550 | * Just calls the close method but ignores if the window is already closed. 551 | * @param L Lua state 552 | * @return Number of return values on the Lua stack 553 | */ 554 | static int window_gc(lua_State *L) { 555 | window *p_window = check_window(L); 556 | 557 | // ignore if the window is already closed 558 | if (!is_window_closed(p_window)) { 559 | window_close(L); 560 | } 561 | 562 | return 0; 563 | } 564 | 565 | /** 566 | * Returns a string representation of the window userdata. 567 | * @param L Lua state 568 | * @return Number of return values on the Lua stack 569 | */ 570 | static int window_tostring(lua_State *L) { 571 | window *p_window = check_window(L); 572 | 573 | if (is_window_closed(p_window)) { 574 | lua_pushliteral(L, "window (closed)"); 575 | } else { 576 | lua_pushfstring(L, "window (%p)", p_window); 577 | } 578 | return 1; 579 | } 580 | 581 | /** Functions for the fenster Lua module */ 582 | static const struct luaL_Reg lfenster_functions[] = { 583 | {"open", lfenster_open}, 584 | {"sleep", lfenster_sleep}, 585 | {"time", lfenster_time}, 586 | {"rgb", lfenster_rgb}, 587 | 588 | // methods can also be used as functions with the userdata as first argument 589 | {"close", window_close}, 590 | {"loop", window_loop}, 591 | {"set", window_set}, 592 | {"get", window_get}, 593 | {"clear", window_clear}, 594 | 595 | {NULL, NULL}}; 596 | 597 | /** Methods for the window userdata */ 598 | static const struct luaL_Reg window_methods[] = { 599 | {"close", window_close}, 600 | {"loop", window_loop}, 601 | {"set", window_set}, 602 | {"get", window_get}, 603 | {"clear", window_clear}, 604 | 605 | // metamethods 606 | {"__index", window_index}, 607 | {"__gc", window_gc}, 608 | #if LUA_VERSION_NUM >= 504 609 | {"__close", window_gc}, 610 | #endif 611 | {"__tostring", window_tostring}, 612 | 613 | {NULL, NULL}}; 614 | 615 | /** 616 | * Entry point for the fenster Lua module. 617 | * @param L Lua state 618 | * @return Number of return values on the Lua stack 619 | */ 620 | FENSTER_EXPORT int luaopen_fenster(lua_State *L) { 621 | // create the window metatable 622 | const int result = luaL_newmetatable(L, WINDOW_METATABLE); 623 | if (result == 0) { 624 | return luaL_error(L, "fenster metatable already exists (%s)", 625 | WINDOW_METATABLE); 626 | } 627 | luaL_setfuncs(L, window_methods, 0); 628 | 629 | // create and return the fenster Lua module 630 | luaL_newlib( // NOLINT(readability-math-missing-parentheses) 631 | L, lfenster_functions); 632 | return 1; 633 | } 634 | --------------------------------------------------------------------------------