├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SPEC.md ├── build.sh ├── lib └── microtar │ ├── LICENSE │ ├── README.md │ └── src │ ├── microtar.c │ └── microtar.h ├── libraries └── json.lua ├── lite-xl-plugin-manager.json ├── lpm.json ├── manifest.json ├── meson.build ├── meson_options.txt ├── plugins ├── plugin_manager │ ├── init.lua │ └── plugin_view.lua └── welcome.lua ├── src ├── lpm.c └── lpm.lua └── t └── run.lua /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: { branches: ['!master'] } 4 | pull_request: { branches: ['*'] } 5 | workflow_dispatch: 6 | workflow_call: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | config: 16 | - { platform: linux, arch: x86_64, native: true } 17 | - { platform: linux, arch: aarch64, native: false } 18 | - { platform: linux, arch: riscv64, native: false } 19 | - { platform: windows, arch: x86_64, suffix: .exe, native: false } 20 | - { platform: android, arch: arm, abi: armeabi-v7a, llvm: armv7a, eabi: eabi, native: false } 21 | - { platform: android, arch: aarch64, abi: arm64-v8a, llvm: aarch64, native: false } 22 | - { platform: android, arch: x86_64, abi: x86_64, llvm: x86_64, native: false } 23 | - { platform: android, arch: x86, abi: x86, llvm: i686, native: false } 24 | 25 | steps: 26 | - name: Checkout Code 27 | uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 0 30 | submodules: true 31 | 32 | - name: Set Environment Variables 33 | run: | 34 | echo VERSION=`git describe --tags --abbrev=0 --match "v*" | tail -c +2` >> $GITHUB_ENV 35 | echo FULL_VERSION=`git describe --tags --match "v*" | tail -c +2` >> $GITHUB_ENV 36 | echo ARCH=${{ matrix.config.arch }}-${{ matrix.config.platform }} >> $GITHUB_ENV 37 | echo BIN=lpm.${{ matrix.config.arch }}-${{ matrix.config.platform }}${{ matrix.config.suffix }} >> $GITHUB_ENV 38 | echo HOSTCC=gcc >> $GITHUB_ENV 39 | 40 | - name: Setup (Linux) 41 | if: ${{ matrix.config.platform == 'linux' && matrix.config.native }} 42 | run: | 43 | sudo apt-get update && sudo apt-get install musl-tools musl musl-dev 44 | echo CC=musl-gcc >> $GITHUB_ENV 45 | 46 | - name: Setup (Linux cross-compilation) 47 | if: ${{ matrix.config.platform == 'linux' && ! matrix.config.native }} 48 | run: | 49 | wget -q https://github.com/cross-tools/musl-cross/releases/download/20250520/${{ matrix.config.arch }}-unknown-linux-musl.tar.xz 50 | unxz ${{ matrix.config.arch }}-unknown-linux-musl.tar.xz && tar -xvf *.tar 51 | echo CC=$(pwd)/${{ matrix.config.arch }}-unknown-linux-musl/bin/${{ matrix.config.arch }}-unknown-linux-musl-cc >> $GITHUB_ENV 52 | echo AR=$(pwd)/${{ matrix.config.arch }}-unknown-linux-musl/bin/${{ matrix.config.arch }}-unknown-linux-musl-ar >> $GITHUB_ENV 53 | 54 | - name: Setup (Windows) 55 | if: ${{ matrix.config.platform == 'windows' }} 56 | run: | 57 | sudo apt-get update && sudo apt-get install mingw-w64 58 | CMAKE_DEFAULT_FLAGS=( 59 | "-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER" 60 | "-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=NEVER" "-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=NEVER" 61 | "-DCMAKE_POSITION_INDEPENDENT_CODE=ON" "-DCMAKE_SYSTEM_NAME=Windows" 62 | "-DCMAKE_SYSTEM_INCLUDE_PATH=/usr/share/mingw-w64/include" 63 | ) 64 | echo "CMAKE_DEFAULT_FLAGS=${CMAKE_DEFAULT_FLAGS[*]}" >> $GITHUB_ENV 65 | echo "LZMA_CONFIGURE=--host=x86_64-w64-mingw32" >> $GITHUB_ENV 66 | echo "GIT2_CONFIGURE=-DDLLTOOL=x86_64-w64-mingw32-dlltool" >> $GITHUB_ENV 67 | echo CC=x86_64-w64-mingw32-gcc >> $GITHUB_ENV 68 | echo AR=x86_64-w64-mingw32-gcc-ar >> $GITHUB_ENV 69 | echo WINDRES=x86_64-w64-mingw32-windres >> $GITHUB_ENV 70 | 71 | - name: Setup (Android) 72 | if: ${{ matrix.config.platform == 'android' }} 73 | env: { ANDROID_ABI_VERSION: "26" } 74 | run: | 75 | LLVM_BIN="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin" 76 | CMAKE_DEFAULT_FLAGS=( 77 | "-DCMAKE_ANDROID_NDK=$ANDROID_NDK_HOME" 78 | "-DCMAKE_ANDROID_API=$ANDROID_ABI_VERSION" "-DCMAKE_SYSTEM_VERSION=$ANDROID_ABI_VERSION" 79 | "-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=NEVER" "-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=NEVER" 80 | "-DCMAKE_SYSTEM_NAME=Android" "-DCMAKE_SYSTEM_INCLUDE_PATH=$ANDROID_SYSROOT_NDK/sysroot/usr/include" 81 | "-DCMAKE_ANDROID_ARCH_ABI=${{ matrix.config.abi }}" 82 | ) 83 | echo "CMAKE_DEFAULT_FLAGS=${CMAKE_DEFAULT_FLAGS[*]}" >> $GITHUB_ENV 84 | echo "AR=$LLVM_BIN/llvm-ar" >> $GITHUB_ENV 85 | echo "CC=$LLVM_BIN/${{ matrix.config.llvm }}-linux-android${{ matrix.config.eabi }}$ANDROID_ABI_VERSION-clang" >> $GITHUB_ENV 86 | echo "CFLAGS=-Dinline=" >> $GITHUB_ENV 87 | 88 | - name: Build 89 | run: | 90 | ./build.sh clean && ./build.sh -DLPM_STATIC -DLPM_VERSION='"'$FULL_VERSION-$ARCH'"' -static -O3 -ffunction-sections -fdata-sections -Wl,--gc-sections 91 | 92 | - name: Run Tests 93 | if: ${{ matrix.config.native }} 94 | run: | 95 | cp $BIN lpm && ./lpm test t/run.lua 96 | 97 | # - name: Package Debian/Ubuntu 98 | # env: { ARCH: "amd64", DESCRIPTION: "A plugin manager for the lite-xl text editor.", MAINTAINER: "Adam Harrison " } 99 | # run: | 100 | # export NAME=lpm_$VERSION.0-$REV""_$ARCH 101 | # mkdir -p $NAME/usr/bin $NAME/DEBIAN && cp lpm $NAME/usr/bin 102 | # printf "Package: lpm\nVersion: $VERSION\nArchitecture: $ARCH\nMaintainer: $MAINTAINER\nDescription: $DESCRIPTION\n" > $NAME/DEBIAN/control 103 | # dpkg-deb --build --root-owner-group $NAME 104 | 105 | - name: Upload Artifacts 106 | uses: actions/upload-artifact@v4 107 | with: 108 | path: ${{ env.BIN }} 109 | name: ${{ env.BIN }} 110 | 111 | build-macos: 112 | strategy: 113 | matrix: 114 | config: 115 | - { arch: x86_64, runner: macos-13 } # macos-13 runs on Intel runners 116 | - { arch: aarch64, runner: macos-14 } # macos-14 runs on M1 runners 117 | 118 | runs-on: ${{ matrix.config.runner }} 119 | env: { CC: clang } 120 | 121 | steps: 122 | - name: Checkout code 123 | uses: actions/checkout@v4 124 | with: 125 | fetch-depth: 0 126 | submodules: true 127 | 128 | - name: Set Environment Variables 129 | run: | 130 | echo VERSION=`git describe --tags --abbrev=0 --match "v*" | tail -c +2` >> $GITHUB_ENV 131 | echo REV=$((`git describe --tags --match "v*" | sed 's/.*-\([0-9]*\)-.*/\1/' | sed s/^v.*//` + 1)) >> $GITHUB_ENV 132 | echo ARCH=${{ matrix.config.arch }}-darwin >> $GITHUB_ENV 133 | echo BIN=lpm.${{ matrix.config.arch }}-darwin >> $GITHUB_ENV 134 | 135 | - name: Build 136 | run: | 137 | ./build.sh clean && ./build.sh -DLPM_STATIC -DLPM_VERSION='"'$FULL_VERSION-$ARCH'"' -O3 138 | 139 | - name: Upload Artifacts 140 | uses: actions/upload-artifact@v4 141 | with: 142 | path: ${{ env.BIN }} 143 | name: ${{ env.BIN }} 144 | 145 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: { branches: [master] } 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build-everything: 8 | uses: ./.github/workflows/build.yml 9 | secrets: inherit 10 | 11 | create-release: 12 | runs-on: ubuntu-latest 13 | needs: build-everything 14 | env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" } 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Set Environment Variables 23 | run: | 24 | echo VERSION=`git describe --tags --abbrev=0 --match "v*" | tail -c +2` >> $GITHUB_ENV 25 | 26 | - name: Download Artifacts 27 | uses: actions/download-artifact@v4 28 | with: 29 | pattern: lpm.* 30 | path: artifacts 31 | merge-multiple: true 32 | 33 | - name: Create Release(s) 34 | run: | 35 | perl -pe 'last if $_ =~ m/^\s*#/ && $_ !~ m/#\s*$ENV{VERSION}/' < CHANGELOG.md | tail -n +2 > NOTES.md 36 | gh release delete -y continuous || true; 37 | gh release create -t 'Continuous Release' -F NOTES.md continuous ./artifacts/* 38 | if [[ `git tag --points-at HEAD v* | head -c 1` == "v" ]]; then 39 | gh release delete -y v$VERSION || true; 40 | gh release create -t v$VERSION -F NOTES.md v$VERSION ./artifacts/* 41 | gh release delete -y latest || true; 42 | gh release create -t latest -F NOTES.md latest ./artifacts/* 43 | git branch -f latest HEAD 44 | git tag -f latest 45 | git push -f origin refs/heads/latest 46 | git push -f origin refs/tags/latest 47 | fi 48 | git tag -f continuous 49 | git push -f origin refs/tags/continuous 50 | 51 | - name: Discord Notification 52 | env: { DISCORD_WEBHOOK: "${{ secrets.DISCORD_WEBHOOK }}" } 53 | run: | 54 | if [[ -n "$DISCORD_WEBHOOK" ]] && [[ `git tag --points-at HEAD v* | head -c 1` == "v" ]]; then 55 | perl -e 'use JSON qw(encode_json from_json); $/ = undef; print encode_json({ content => "## Lite XL Plugin Manager $ENV{VERSION} has been released!\nhttps://github.com/lite-xl/lite-xl-plugin-manager/releases/tag/v$ENV{VERSION}\n### Changes in $ENV{VERSION}:\n" . <> })' < NOTES.md | 56 | curl -H 'Content-Type:application/json' $DISCORD_WEBHOOK -X POST -d "$( 100 character filenames. Thank you @Gaspartcho! 144 | * Fixed bug that tried to uninstall core depednencies. Thanks @Gaspartcho! 145 | * Fixed issue with `lpm` not correctly renaming bottles, or moving files around. 146 | * Added in ability to `--mask`, so that you can explicitly cut out dependencies that you think aren't requried on install/uninstall. 147 | * Made `--ephemeral` bottles have distinct hashes from non-epehemeral ones. 148 | * Fixed a bug where we tried to double-install depdendencies if they were explicitly specified in the install command. 149 | 150 | # 1.1.0 151 | 152 | * Added in `font` as a new `type` for addons. 153 | * Fixed a bug that made it so that complex plugins that didn't specify a path would clone their repos, instead of just downloading the listed files. 154 | * Fixed bugs around specifying a lite-xl to add to the system. 155 | * Added documentation for `lpm hash`. 156 | * Added in ability to automatically update checksums in manifests under certain circumstances with `lpm update-checksums`. 157 | * Improved handling around adding disparate versions of lite-xl with binary, data and user directories in different places. 158 | 159 | # 1.0.14 160 | 161 | * Fixed some spelling errors. 162 | * Removed `system.revparse`. 163 | * Allowed fetch to automatically determine the default branch of a remote; returns as part of `fetch`. 164 | * Fixed an error that prevented SSL certificates present in a directory from working. 165 | 166 | # 1.0.13 167 | 168 | * Merged in `welcome.lua` as a plugin. 169 | * Added in ability to specify `--ephemeral` when running bottles; cleans up the bottle when lite-xl exits. 170 | * Improved error handling by removing unecessary line numbers. 171 | * Made running of `lpm` more deterministic. 172 | * Made it so that we only `fetch` when necessary in order to speed things up. 173 | * Fixed some errors where cache wasn't being invaldiated approprirately. 174 | * Allowed for short looks up when referencing commit ids. 175 | 176 | # 1.0.12 177 | 178 | * Updated meson to properly retrieve mbedtls2 when compiling. 179 | * Added in `exec` to run lua files. 180 | * Changed how arguments are interpreted when using `test` or `exec`. 181 | * Fixed bug with windows not properly flushing files on first run. 182 | * Moved lockfile to `CACHEDIR` from `USERDIR`. 183 | * Fixed some caching issues when something failed to install during bottle construction. 184 | * Fixed issues around filtering/matching when using `list`. 185 | * Added in table output format that allows you to generically specify a table and column list you want. 186 | * Added in `unstub` command. 187 | * Added in `--repository` flag. 188 | 189 | # 1.0.11 190 | 191 | * Fixed an issue with constructing bottles when not specifying a config. 192 | * Fixed a major issue when installing packages with distinct versions (like `plugin_manager` does). 193 | * Thanks to @guldoman for both fixes! 194 | 195 | # 1.0.10 196 | 197 | * We now fully clear bottles when reconstructing them avoiding confusion/bugs over old bottles. 198 | * A `--config` flag has been added that allows you to specify a user config when running. 199 | 200 | # 1.0.9 201 | 202 | * `lpm` now automatically extracts and chmod's `.gz` files. 203 | * Added in preprocessor guard for shallow cloning to allow non-bleeding-edge `libgit2` linkings. 204 | * Fixed bug where dangling symlinks of lite binaries in `$PATH` would cause an error. 205 | 206 | # 1.0.8 207 | 208 | * Autoflush stderr, so that certain windows terminals don't get blank prompts. 209 | * Added method to grab and install orphan plugins from one-off repos. 210 | * Passed debug build status through to underlying libraries. 211 | * Fixed bug where we compared sizes of folders to determined if they were the same. 212 | * Made it so you can set the define LPM_DEFAULT_REPOSITORY if you want to build a custom manager. 213 | * Made it so tests run more smoothly, and will always use the lpm you compiled, instead of system lpm. 214 | * Normalized paths on windows to backslashes for consistency. 215 | 216 | # 1.0.7 217 | 218 | * Upgraded submodules. 219 | * Moved mbedtls to a submodule, as there was an erroneous resaon why it wasn't, and upgraded it to fix #33, which occurs due to a clang compiler bug. 220 | * Improved debuggability with regards to tls and the `--trace` flag. 221 | * Fixed some small bugs with `plugin_manager` that nonetheless rendered it inoperable. 222 | * Fixed issues with getting the absolute path of symlinks. 223 | * Fixed issues with symlinks that are broken not getting detected at all. 224 | * Allowed for dashes in auto-generated ids. 225 | * Fixed a bug that stopped things form working when explicitly calling `init`. 226 | * Allowed `run` to use `--remotes`. 227 | * Fixed bug for auto-detecting data directories, when determining system `lite-xl`. 228 | 229 | # 1.0.6 230 | 231 | * Changed from full git cloning to shallow cloning. 232 | * Fixed major bug on windows. 233 | * Moved `json` to the `libraries` folder. 234 | 235 | # 1.0.5 236 | 237 | * Marked `lpm` for `plugin_manager` as optional. 238 | * Made `--help` and `help` output on `stdout`, rather than `stderr`, following convention. 239 | * Removed system configuration search paths for `git`. 240 | * Removed `xxd` as a build dependency. 241 | * Colorized some extra messages. 242 | * Made repository fetching atomic. 243 | * Made sure that `common.path` checked for executability and non-folderness. 244 | * Added in meson as a build system (thank you @Jan200101). 245 | 246 | # 1.0.4 247 | 248 | * Added in metapackage support into manifest and SPEC. 249 | * Fixed issue with system lite-xls not being detected correctly. 250 | * Colorized output by default. 251 | * Added in NO_COLOR standard. 252 | * Updated SPEC and fixed a few spelling/grammatical errors. 253 | 254 | # 1.0.3 255 | 256 | * Fixed a major issue with windows that causes a crash. 257 | * Ensured that the simplified releases are pointing to the right place. 258 | 259 | # 1.0.2 260 | 261 | * Suppresses the progress bar by default if we're not on a TTY. 262 | * Added `url` as a field to `SPEC.md`. 263 | * Modified `run` so that it'll use the system version if you don't specify one. 264 | * Added the ability to specify a repo url as part of `run`, so you can easily test new plugin branches and their plugins without actually modifying your lpm state. 265 | * Fixed a few typos. 266 | * Fixed issue with `run` not handling cases where plugins were either orphaned or core plugins, which would cause the bottle to be incorrectly constructed. 267 | * Fixed issue where you could add non-numeric lite versions. 268 | * Fixed issue where tables generated with lpm didn't annotate non-remote url plugins with \*. 269 | * Fixed a memory leak. 270 | * Added in warning to let people know when stubs are mismatching versions. 271 | * Added in warning when we cannot acquire an lpm global lock, and also made it so we do not lock upon running something. 272 | * Better error handling for invalid manifests, specifically when paths for plugins don't exist. 273 | * Fixed issue with permissions not being recorded correctly when extracting from a zip file. 274 | * Added in --reinstall flag. 275 | 276 | 277 | # 1.0.1 278 | 279 | * Fixed an issue with --no-install-optional being non-functional. 280 | * Modified fopen calls to use `_wfopen` where appropriate to improve UTF-8 support on windows. 281 | * Fixed some defaults around specifiying explicit binaries and datadirs for certain pathways. 282 | * Added this CHANGELOG.md. 283 | 284 | # 1.0.0 285 | 286 | Initial release of `lpm`. 287 | 288 | ``` 289 | Usage: lpm COMMAND [...ARGUMENTS] [--json] [--userdir=directory] 290 | [--cachedir=directory] [--quiet] [--version] [--help] [--remotes] 291 | [--ssl-certs=directory/file] [--force] [--arch=x86_64-linux] 292 | [--assume-yes] [--no-install-optional] [--verbose] [--mod-version=3] 293 | [--datadir=directory] [--binary=path] [--symlink] [--post] 294 | 295 | LPM is a package manager for `lite-xl`, written in C (and packed-in lua). 296 | 297 | It's designed to install packages from our central github repository (and 298 | affiliated repositories), directly into your lite-xl user directory. It can 299 | be called independently, for from the lite-xl `addon_manager` addon. 300 | 301 | LPM will always use https://github.com/lite-xl/lite-xl-plugin-manager as its base 302 | repository, if none are present, and the cache directory does't exist, 303 | but others can be added, and this base one can be removed. 304 | 305 | It has the following commands: 306 | 307 | lpm init [repo 1] [repo 2] [...] Implicitly called before all commands 308 | if necessary, but can be called 309 | independently to save time later, or 310 | to set things up differently. 311 | 312 | Adds the built in repository to your 313 | repository list, and all `remotes`. 314 | 315 | If repo 1 ... is specified, uses that 316 | list of repositories as the base instead. 317 | 318 | If "none" is specified, initializes 319 | an empty repository list. 320 | 321 | lpm repo list List all extant repos. 322 | lpm [repo] add Add a source repository. 323 | [...] 324 | lpm [repo] rm Remove a source repository. 325 | [...] 326 | lpm [repo] update [] Update all/the specified repos. 327 | [...] 328 | lpm [plugin|library|color] install Install specific addons. 329 | [:] If installed, upgrades. 330 | [...:] 331 | lpm [plugin|library|color] uninstall Uninstall the specific addon. 332 | [...] 333 | lpm [plugin|library|color] reinstall Uninstall and installs the specific addon. 334 | [...] 335 | 336 | lpm [plugin|library|color] list List all/associated addons. 337 | [...] 338 | 339 | lpm upgrade Upgrades all installed addons 340 | to new version if applicable. 341 | lpm [lite-xl] install Installs lite-xl. Infers the 342 | [binary] [datadir] paths on your system if not 343 | supplied. Automatically 344 | switches to be your system default 345 | if path auto inferred. 346 | lpm lite-xl add Adds a local version of lite-xl to 347 | the managed list, allowing it to be 348 | easily bottled. 349 | lpm lite-xl remove Removes a local version of lite-xl 350 | from the managed list. 351 | lpm [lite-xl] switch [] Sets the active version of lite-xl 352 | to be the specified version. Auto-detects 353 | current install of lite-xl; if none found 354 | path can be specified. 355 | lpm lite-xl list [name pattern] Lists all installed versions of 356 | [...filters] lite-xl. Can specify the flags listed 357 | in the filtering seciton. 358 | lpm run [...addons] Sets up a "bottle" to run the specified 359 | lite version, with the specified addons 360 | and then opens it. 361 | lpm describe [bottle] Describes the bottle specified in the form 362 | of a list of commands, that allow someone 363 | else to run your configuration. 364 | lpm table [readme path] Formats a markdown table of all specified 365 | addons. Dumps to stdout normally, but if 366 | supplied a readme, will remove all tables 367 | from the readme, and append the new one. 368 | 369 | lpm purge Completely purge all state for LPM. 370 | lpm - Read these commands from stdin in 371 | an interactive print-eval loop. 372 | lpm help Displays this help text. 373 | 374 | 375 | Flags have the following effects: 376 | 377 | --json Performs all communication in JSON. 378 | --userdir=directory Sets the lite-xl userdir manually. 379 | If omitted, uses the normal lite-xl logic. 380 | --cachedir=directory Sets the directory to store all repositories. 381 | --tmpdir=directory During install, sets the staging area. 382 | --datadir=directory Sets the data directory where core addons are located 383 | for the system lite-xl. 384 | --binary=path Sets the lite-xl binary path for the system lite-xl. 385 | --verbose Spits out more information, including intermediate 386 | steps to install and whatnot. 387 | --quiet Outputs nothing but explicit responses. 388 | --mod-version=version Sets the mod version of lite-xl to install addons. 389 | --version Returns version information. 390 | --help Displays this help text. 391 | --ssl-certs Sets the SSL certificate store. Can be a directory, 392 | or path to a certificate bundle. 393 | --arch=architecture Sets the architecture (default: x86_64-linux). 394 | --assume-yes Ignores any prompts, and automatically answers yes 395 | to all. 396 | --no-install-optional On install, anything marked as optional 397 | won't prompt. 398 | --trace Dumps to STDERR useful debugging information, in 399 | particular information relating to SSL connections, 400 | and other network activity. 401 | --progress For JSON mode, lines of progress as JSON objects. 402 | By default, JSON does not emit progress lines. 403 | --symlink Use symlinks where possible when installing modules. 404 | If a repository contains a file of the same name as a 405 | `files` download in the primary directory, will also 406 | symlink that, rather than downloading. 407 | 408 | The following flags are useful when listing plugins, or generating the plugin 409 | table. Putting a ! infront of the string will invert the filter. Multiple 410 | filters of the same type can be specified to create an OR relationship. 411 | 412 | --author=author Only display addons by the specified author. 413 | --tag=tag Only display addons with the specified tag. 414 | --stub=git/file/false Only display the specified stubs. 415 | --dependency=dep Only display addons that have a dependency on the 416 | specified addon. 417 | --status=status Only display addons that have the specified status. 418 | --type=type Only display addons on the specified type. 419 | --name=name Only display addons that have a name which matches the 420 | specified filter. 421 | 422 | There also several flags which are classified as "risky", and are never enabled 423 | in any circumstance unless explicitly supplied. 424 | 425 | --force Ignores checksum inconsistencies. 426 | --post Run post-install build steps. Must be explicitly enabled. 427 | Official repositories must function without this 428 | flag being needed; generally they must provide 429 | binaries if there is a native compilation step. 430 | --remotes Automatically adds any specified remotes in the 431 | repository to the end of the resolution list. 432 | --ssl-certs=noverify Ignores SSL certificate validation. Opens you up to 433 | man-in-the-middle attacks. 434 | 435 | There exist also other debug commands that are potentially useful, but are 436 | not commonly used publically. 437 | 438 | lpm test [test file] Runs the specified test suite. 439 | lpm table [...filters] Generates markdown table for the given 440 | manifest. Used by repositories to build 441 | READMEs. 442 | lpm download [target] Downloads the specified URL to stdout, 443 | or to the specified target file. 444 | lpm extract Extracts the specified archive at 445 | [target] target, or the current working directory. 446 | ``` 447 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ## LPM License 2 | 3 | Copyright (c) 2022 lite-xl Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | ## Lua JSON Library 24 | 25 | Copyright (c) 2020 rxi 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining a copy of 28 | this software and associated documentation files (the "Software"), to deal in 29 | the Software without restriction, including without limitation the rights to 30 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 31 | of the Software, and to permit persons to whom the Software is furnished to do 32 | so, subject to the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be included in all 35 | copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 38 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 39 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 40 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 41 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 42 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 43 | SOFTWARE. 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lite XL Plugin Manager (lpm) 2 | 3 | ![image](https://user-images.githubusercontent.com/1034518/216748882-3ae8c8d4-a767-4d97-acc4-c1cde7e3e331.png) 4 | 5 | A standalone binary that provides an easy way of installing, and uninstalling 6 | plugins from lite-xl, as well as different version of lite-xl. 7 | 8 | Can be used by a package manager plugin that works from inside the editor 9 | and calls this binary. 10 | 11 | Also contains a `plugin_manager.lua` plugin to integrate the binary with lite-xl in 12 | the form of an easy-to-use GUI. 13 | 14 | By default in releases, `lpm` will automatically consume the `manifest.json` 15 | in the `latest` branch of this repository, which corresponds to the most 16 | recent versioned release. 17 | 18 | As of Lite XL version 3, `lpm`, along with the `plugin_manager` and `welcome` 19 | plugins will be bundled with all official releases. 20 | 21 | Conforms to [SCPS3](https://github.com/adamharrison/straightforward-c-project-standard#SCPS3). 22 | 23 | ## Status 24 | 25 | `lpm` 1.0 has been just released, and so may still contain bugs, but is generally feature-complete. 26 | 27 | ## Specification 28 | 29 | For details about the `manifest.json` files that `lpm` consumes, 30 | [see here](SPEC.md). 31 | 32 | ## Installing 33 | 34 | ### Windows 35 | 36 | On Windows, the best way to start, especially if you're used to a visual interface is to simply pull 37 | `lpm`, and use it to install `plugin_manager`, which will give you a graphical plugin manager inside 38 | Lite XL that can be accessed with the `Plugin Manager: Show` command (`ctrl+shift+p`). 39 | 40 | To get started, open a PowerShell terminal, and run the following: 41 | 42 | ```powershell 43 | Invoke-WebRequest -Uri "https://github.com/lite-xl/lite-xl-plugin-manager/releases/download/latest/lpm.x86_64-windows.exe" -OutFile "lpm.exe" 44 | .\lpm.exe install plugin_manager --assume-yes 45 | Remove-Item lpm.exe 46 | ``` 47 | 48 | This should install `lpm` and install the GUI. If you already had Lite XL open, please restart it (`ctrl+alt+r`). 49 | 50 | ### Linux + Mac 51 | 52 | The fastest way to get started with lpm is to simply pull a release. 53 | 54 | ```sh 55 | wget https://github.com/lite-xl/lite-xl-plugin-manager/releases/download/latest/lpm.x86_64-linux -O lpm && chmod +x lpm 56 | ``` 57 | 58 | If you want to get the GUI version installed with lite-xl, you can tell `lpm` to install `plugin_manager`, which will allow 59 | you to access `Plugin Manager: Show` in the command palette in `lite-xl`. 60 | 61 | ```sh 62 | ./lpm install plugin_manager --assume-yes 63 | ``` 64 | 65 | ### Compilation 66 | 67 | If you have a C compiler, and `git`, and want to compile from scratch, 68 | you can do: 69 | 70 | ```sh 71 | git clone https://github.com/lite-xl/lite-xl-plugin-manager.git \ 72 | --shallow-submodules --recurse-submodules && cd lite-xl-plugin-manager &&\ 73 | ./build.sh -DLPM_STATIC && ./lpm 74 | ```` 75 | 76 | If you want to build it quickly, and have the right modules installed, you can 77 | do: 78 | 79 | ```sh 80 | ./build.sh -lgit2 -lzip -llua -lm -lmbedtls -lmbedx509 -lmbedcrypto -lz -DLPM_STATIC 81 | ``` 82 | 83 | OR 84 | 85 | ```sh 86 | gcc src/lpm.c lib/microtar/src/microtar.c -Ilib/microtar/src -lz -lgit2 \ 87 | -lzip -llua -lm -lmbedtls -lmbedx509 -lmbedcrypto -o lpm 88 | ``` 89 | 90 | CI is enabled on this repository, so you can grab Windows and Linux builds from the 91 | `continuous` [release page](https://github.com/lite-xl/lite-xl-plugin-manager/releases/tag/continuous), 92 | which is a nightly, or the `latest` [release page](https://github.com/lite-xl/lite-xl-plugin-manager/releases/tag/latest), 93 | which holds the most recent released version. 94 | 95 | There are also tagged releases, for specified versions. 96 | 97 | You can get a feel for how to use `lpm` by typing `./lpm --help`. 98 | 99 | You can also use `scoop` to grab `lpm`: 100 | 101 | ``` 102 | scoop install https://raw.githubusercontent.com/lite-xl/lite-xl-plugin-manager/refs/heads/master/lite-xl-plugin-manager.json 103 | ``` 104 | 105 | Please note, that _meson_ is _not_ necessarily the best way to compile `lpm`. If you have troubles with it, please do consider using the build.sh script. 106 | 107 | ## Supporting Libraries / Dependencies 108 | 109 | As seen in the `lib` folder, the following external libraries are used to 110 | build `lpm` as git submodules: 111 | 112 | * `lua` (core program written in) 113 | * `mbedtls` (https/SSL support) 114 | * `libgit2` (accessing git repositories directly) 115 | * `libz` (supporting library for everything) 116 | * `libzip` (for unpacking .zip files) 117 | * `libmicrotar` (for unpacking .tar.gz files) 118 | 119 | To build, `lpm` only requires a C compiler. To run the underlying build process 120 | for `mbedtls` and `libgit2`, `cmake` is also required. 121 | 122 | ## Supported Platforms 123 | 124 | `lpm` should work on all platforms `lite-xl` works on; but releases are offered for the following: 125 | 126 | * Windows x86_64 127 | * Linux x86_64 128 | * Linux aarch64 129 | * MacOS x86_64 130 | * MacOS aarch64 131 | * Android 8.0+ x86_64 132 | * Android 8.0+ x86 133 | * Android 8.0+ aarch64 134 | * Android 8.0+ armv7a 135 | 136 | Experimental support (i.e. doesn't work) exists for the following platforms: 137 | 138 | * Linux riscv64 139 | 140 | ## Use in CI 141 | 142 | To make pre-fab lite builds, you can easily use `lpm` in CI. If you had a linux build container, you could do something like: 143 | 144 | ```sh 145 | curl https://github.com/adamharrison/lite-xl-plugin-manager/releases/download/v0.1/lpm.x86_64-linux > lpm 146 | export LITE_USERDIR=lite-xl/data && export LPM_CACHE=/tmp/cache 147 | ./lpm add https://github.com/adamharrison/lite-xl-plugin-manager && ./lpm install plugin_manager lsp 148 | ``` 149 | 150 | ## Usage 151 | 152 | ```sh 153 | lpm install aligncarets 154 | lpm uninstall aligncarets 155 | ``` 156 | 157 | ```sh 158 | lpm --help 159 | ``` 160 | 161 | ## Building & Running 162 | 163 | ### Linux & MacOS & Windows MSYS 164 | 165 | ``` 166 | ./build.sh clean && ./build.sh -DLPM_STATIC && ./lpm 167 | ``` 168 | 169 | ### Linux -> Windows 170 | 171 | ``` 172 | ./build.sh clean && HOSTCC=gcc CC=x86_64-w64-mingw32-gcc AR=x86_64-w64-mingw32-gcc-ar WINDRES=x86_64-w64-mingw32-windres \ 173 | CMAKE_DEFAULT_FLAGS="-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER\ -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=NEVER -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=NEVER -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_SYSTEM_NAME=Windows -DCMAKE_SYSTEM_INCLUDE_PATH=/usr/share/mingw-w64/include"\ 174 | GIT2_CONFIGURE="-DDLLTOOL=x86_64-w64-mingw32-dlltool" ./build.sh -DLPM_STATIC -DLPM_VERSION='"'$VERSION-x86_64-windows-`git rev-parse --short HEAD`'"' 175 | ``` 176 | 177 | ## Tests 178 | 179 | To run the test suite, you can use `lpm` to execute the test by doing `./lpm test t/run.lua`. use `FAST=1 ./lpm test t/run.lua` to avoid the costs of tearing down and building up suites each time. 180 | 181 | ## Extra Features 182 | 183 | ### Bottles 184 | 185 | ### Extra Fields 186 | 187 | * `addons.files.extra.chmod_executable` 188 | 189 | An array of files to be marked as executable (after extraction, if applicable). 190 | 191 | ## Bugs 192 | 193 | If you find a bug, please create an issue with the following information: 194 | 195 | * Your operating system. 196 | * The commit or version of LPM you're using (`lpm --version` for releases). 197 | * The exact steps to reproduce in LPM invocations, if possible from a fresh LPM install (targeting an empty folder with `--userdir`). 198 | -------------------------------------------------------------------------------- /SPEC.md: -------------------------------------------------------------------------------- 1 | # Manifest Specification 2 | 3 | A lite-xl manifest is a JSON file containing three different keys: 4 | 5 | * Remotes 6 | * Addons 7 | * Lite-XLs 8 | 9 | ## Remotes 10 | 11 | A simple array of string repository identifiers. A repository identifier takes 12 | the form of a git remote url, i.e. `:`. An example would be: 13 | 14 | `https://github.com/lite-xl/lite-xl-plugin-manager.git:latest` 15 | 16 | ## Addons 17 | 18 | Addons are the primary objects specified in this specification. An addon 19 | consists of a series of metadata, the path to the addon in this repository, 20 | or its location on a remote repository, or a publically accessible URL, and a 21 | set of files to be downloaded with the plugin (usually releases, but can be 22 | data files, or fonts, or anything else). 23 | 24 | Addons can optionally specify a type, which determines where they're 25 | installed. Currently three types are supported: 26 | 27 | * `library` 28 | * `plugin` 29 | * `color` 30 | * `font` 31 | * `meta` 32 | 33 | Addons are further classified into two organizational categories. 34 | `singleton` addons, and `complex` addons. Addons are listed a `singleton` 35 | if and only if they consist of exactly one file, have an empty or absent 36 | `files` specification, and do not specify a `remote`. Singleton addons 37 | consist of exactly one `.lua` file, named after the addon. Complex addons 38 | are contained within a folder, and have an `init.lua` or `init.so` file that 39 | loads other components within it. 40 | 41 | The vast majority of addons are `singleton` `plugin`s. 42 | 43 | ### Metadata 44 | 45 | Fields that are required are bolded. 46 | 47 | * **`id`**: The semantic id of the addon, a string only containing `[a-z0-9\-_]`. 48 | * **`version`**: The addon's semantic version (major.minor.revision). A string that matches the regex `^[0-9]+(\.[0-9]+){0,2}$`. 49 | * **`mod_version`**: The mod_version this addon is compatible with. 50 | A string that can contain `[0-9\.]`. If `type` is `library`, this field is optional. 51 | * `type`: An optional string that specifies the addon type. Valid values are `"plugin"` 52 | `"library"`, `"color"`,`"font"`, or `"meta"`. Defaults to `"plugin"`. 53 | * `name`: The optional name of the addon. 54 | * `description`: An optional english-language description of the addon. 55 | * `provides`: An optional array of strings that are a shorthand of functionality 56 | this addon provides. Can be used as a dependency. 57 | * `replaces`: An optional array of ids that this plugin explicitly replaces. Will always 58 | prefer this plugin in place of those plugins, so long as version requirements are met. 59 | * `remote`: Optional. Specifies a public https git link where this addon is located. If present, 60 | denotes a **stub**. 61 | * `dependencies`: Optionally a hash of dependencies required, or optional 62 | for this addon. 63 | * `conflicts`: An optional hash of addons which conflict with this one, in the same 64 | format as `dependencies`. 65 | * `tags`: Optional freeform tags that may describe attributes of the addon. 66 | * `path`: Optional path to the addon. If omitted, will only pull the files in 67 | `files`. To pull the whole repository, use `"."`. 68 | * `arch`: Optionally a list of architectures this plugin supports. If not present, and no `files` that specify arches, assumes that plugin is valid for all architectures. If not present, and at least one `files` exists that specifies an architecture, only assumed to be valid for all `arch`es specified under `files`. Can be either an array of arch names, or can be `"*"` to explicitly specify all architectures. 69 | * `post`: Optionally a string which represents a command to run. If presented 70 | with a dictionary, takes `ARCH` keys, and runs a different command per `ARCH`. 71 | * `url`: Optionally a URL which specifies a direct download link to a single lua file. 72 | precludes the use of `remote`, `path`. Usually a `singleton`. 73 | * `checksum`: Provides a checksum to check against a `url`. 74 | * `extra`: Optionally a dictionary which holds any desired extra information. 75 | 76 | Any keys not present in this official listing render the manifest non-conforming. 77 | Any extra keys should be placed in `extra`. An example of keys that can be placed 78 | in `extra` that some plugin managers/displays will use are: 79 | 80 | * `author`: The main author of the addon. 81 | * `license`: The license under which the addon is licensed. 82 | 83 | ### Dependencies 84 | 85 | Depedencies are specified in an object, with the key being the `id` of the 86 | addon depended upon, or a `provides` alias. 87 | 88 | Dependency values are an object which contain the following keys: 89 | 90 | * `version`: A version specifier. (see below). 91 | * `optional`: A boolean that determines whether the dependency is optional. 92 | 93 | ### Stubs 94 | 95 | If an addon likes, it can specify a particular `remote`; a publically acessible 96 | git repository, accessed via HTTPS, pinned at a specific commit to be used as a 97 | source for its data. In that case, the package manager must download the repository, 98 | and interpret the manifest file found there to determine the addon's metadata. 99 | 100 | This is known as a stub. 101 | 102 | ### Files 103 | 104 | Files are objects that contain at least two keys, `url`, and `checksum`. They 105 | can also optionally contain the `arch` and `path` keys. 106 | 107 | * `url` represents the URL to grab the particular file from. 108 | * `checksum` is the sha256hex checksum for the file. If `"SKIP"` is specified, the 109 | check is skipped. This is fine for development purposes, but any publically 110 | accessible manifest, should specify a checksum. 111 | * `arch` is the lite-xl/clang architecture tuple that the file is relevant for. 112 | if omitted, file is to be assumed to be valid for all arhcitectures. Can be an array. 113 | * `path` is the location to install this file inside the addon's directory. 114 | * `optional` is a boolean that determines whether the file is an optional addition; 115 | if omitted, the file is assumed to be required. 116 | 117 | If a file is an archive, of either `.zip`, `.xz`, `.gz`, `.tgz`, `.txz`, or `.tar.gz`, 118 | it will automatically be extracted inside the addon's directory. 119 | 120 | ## Lite-XLs 121 | 122 | Lite-XLs represent different version of lite-xl that are registered in this 123 | repository. Lite-XLs has the following metadata, as well as a `files` array. 124 | 125 | * `version`: A version specifier. Must take the form of x(.x)\*(-suffix). 126 | Suffixes can be used to denote different flavours of lite-xl. 127 | * `mod_version`: The modversion the binary corresponds to. 128 | 129 | ### Files 130 | 131 | The files array is identical to that of the `files` array under `addons`. 132 | Conventionally, there should be a single file per architecture that is a 133 | `.tar.gz` or `.zip` containing all necessary files for `lite-xl` to run. 134 | 135 | ## Version Specifiers 136 | 137 | When asking for a version, all fields can use inequality operators to specify 138 | the version to be asked for. As an example, `>=0.1` can be used to specify 139 | that any version greater than `0.1` can be used. 140 | 141 | ## Example File 142 | 143 | ```yaml 144 | { 145 | "addons": [ # The addons array contains a list of all addons registered on this repository. 146 | { 147 | "id": "plugin_manager", # Unique name, used to reference the plugin. 148 | "version": "0.1", # Semantic version. 149 | "description": "A GUI interface to the Adam's lite plugin manager.", # English description of the plugin. 150 | "path": "plugins/plugin_manager", # The path to the plugin in this repository. 151 | "mod_version": "3", # The mod_version this plugin corresponds to. 152 | "provides": [ # A list of small strings that represent functionalities this plugin provides. 153 | "plugin-manager" 154 | ], 155 | "files": [ # A list of files (usually binaries) this plugin requires to function. 156 | { 157 | "url": "https://github.com/adamharrison/lite-xl-plugin-manager/releases/download/v0.1/lpm.x86_64-linux", # A publically accessible URL to download from. 158 | "arch": "x86_64-linux", # The lite-xl/clang target tuple that represents the architecture this file is for. 159 | "checksum": "d27f03c850bacdf808436722cd16e2d7649683e017fe6267934eeeedbcd21096" # the sha256hex checksum that corresponds to this file. 160 | }, 161 | { 162 | "url": "https://github.com/adamharrison/lite-xl-plugin-manager/releases/download/v0.1/lpm.x86_64-windows.exe", 163 | "arch": "x86_64-windows", 164 | "checksum": "2ed993ed4376e1840b0824d7619f2d3447891d3aa234459378fcf9387c4e4680" 165 | } 166 | ], 167 | "dependencies": { 168 | "json": {} # Depeneds on `json`, can be a plugin's name or one of its `provides`. 169 | } 170 | }, 171 | { 172 | "id": "RobotoMono", 173 | "version": "0.1", 174 | "type": "font", 175 | "description": "Roboto Mono font.", 176 | "files": [ # Downloads all files listed here to USERDIR/fonts. 177 | 178 | { 179 | "url": "https://github.com/googlefonts/RobotoMono/raw/26adf5193624f05ba1743797d00bcf0e6bfe624f/fonts/ttf/RobotoMono-Regular.ttf", 180 | "checksum": "7432e74ff02682c6e207be405f00381569ec96aa247d232762fe721ae41b39e2" 181 | } 182 | ] 183 | }, 184 | { 185 | "id": "json", 186 | "version": "1.0", 187 | "description": "JSON support plugin, provides encoding/decoding.", 188 | "type": "library", 189 | "path": "plugins/json.lua", 190 | "provides": [ 191 | "json" 192 | ] 193 | }, 194 | { 195 | "tags": ["language"], 196 | "description": "Syntax for .gitignore, .dockerignore and some other `.*ignore` files", 197 | "version": "1.0", 198 | "mod_version": "3", 199 | "remote": "https://github.com/anthonyaxenov/lite-xl-ignore-syntax:2ed993ed4376e1840b0824d7619f2d3447891d3aa234459378fcf9387c4e4680", # The remote to be used for this plugin. 200 | "id": "language_ignore", 201 | "post": {"x86-linux":"cp language_ignore.lua /tmp/somewhere-else", "x86-windows":"COPY language_ignore.lua C:\\Users\\Someone\\ignore.lua"} # Post download steps to run to fully set up the plugin. Does not run by default, requires --post. 202 | }, 203 | { 204 | "description": "Provides a GUI to manage core and plugin settings, bindings and select color theme. Depends on widget.", 205 | "dependencies": { 206 | "toolbarview": { "version": ">=1.0" }, 207 | "widget": { "version": ">=1.0" } 208 | }, 209 | "version": "1.0", 210 | "mod_version": "3", 211 | "path": "plugins/settings.lua", 212 | "id": "settings" 213 | }, 214 | { 215 | "description": "Syntax for Kaitai struct files", 216 | "url": "https://raw.githubusercontent.com/whiteh0le/lite-plugins/main/plugins/language_ksy.lua?raw=1", # URL directly to the singleton plugin file. 217 | "id": "language_ksy", 218 | "version": "1.0", 219 | "mod_version": "3", 220 | "checksum": "08a9f8635b09a98cec9dfca8bb65f24fd7b6585c7e8308773e7ddff9a3e5a60f", # Checksum for this particular URL. 221 | } 222 | ], 223 | "lite-xls": [ # An array of lite-xl releases. 224 | { 225 | "version": "2.1-simplified", # The version, followed by a release suffix defining the release flavour. The only releases that are permitted to not have suffixes are official relases. 226 | "mod_version": "3", # The mod_version this release corresponds to. 227 | "files": [ # Identical to `files` under `addons`, although these are usually simply archives to be extracted. 228 | { 229 | "arch": "x86_64-linux", 230 | "url": "https://github.com/adamharrison/lite-xl-simplified/releases/download/v2.1/lite-xl-2.1.0-simplified-x86_64-linux.tar.gz", 231 | "checksum": "b5087bd03fb491c9424485ba5cb16fe3bb0a6473fdc801704e43f82cdf960448" 232 | }, 233 | { 234 | "arch": "x86_64-windows", 235 | "url": "https://github.com/adamharrison/lite-xl-simplified/releases/download/v2.1/lite-xl-2.1.0-simplified-x86_64-windows.zip", 236 | "checksum": "f12cc1c172299dd25575ae1b7473599a21431f9c4e14e73b271ff1429913275d" 237 | } 238 | ] 239 | }, 240 | { 241 | "version": "2.1-simplified-enhanced", 242 | "mod_version": "3", 243 | "files": [ 244 | { 245 | "arch": "x86_64-linux", 246 | "url": "https://github.com/adamharrison/lite-xl-simplified/releases/download/v2.1/lite-xl-2.1.0-simplified-x86_64-linux-enhanced.tar.gz", 247 | "checksum": "4625c7aac70a2834ef5ce5ba501af2d72d203441303e56147dcf8bcc4b889e40" 248 | }, 249 | { 250 | "arch": "x86_64-windows", 251 | "url": "https://github.com/adamharrison/lite-xl-simplified/releases/download/v2.1/lite-xl-2.1.0-simplified-x86_64-windows-enhanced.zip", 252 | "checksum": "5ac009e3d5a5c99ca7fbd4f6b5bd4e25612909bf59c0925eddb41fe294ce28a4" 253 | } 254 | ] 255 | } 256 | ], 257 | "remotes": [ # A list of remote specifiers. The plugin manager will pull these in and add them as additional repositories if specified to do so with a flag. 258 | "https://github.com/lite-xl/lite-xl-plugins.git:2.1", 259 | "https://github.com/adamharrison/lite-xl-simplified.git:v2.1" 260 | ] 261 | } 262 | ``` 263 | 264 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | : ${CC=gcc} 4 | : ${HOSTCC=$CC} 5 | : ${AR=ar} 6 | : ${MAKE=make} 7 | : ${BIN=lpm} 8 | : ${JOBS=4} 9 | 10 | # The non-exhaustive build options are available: 11 | # clean Cleans the build directory. 12 | # -g Compile with debug support. 13 | # -l Compiles against the shared system version of the specified library. 14 | # -DLPM_NO_GIT Compiles without libgit2 support. 15 | # -DLPM_NO_NETWORK Compiles without network support. 16 | # -DLPM_NO_THREADS Compiles without threading support. 17 | # -DLPM_NO_REMOTE_EXECUTABLE Compiles without the ability to download remote executables. 18 | # -DLPM_STATIC Compiles lpm.lua into the binary executable. 19 | # -DLPM_ARCH_TUPLE Specifies the arch tuple for this build. 20 | # -DLPM_DEFAULT_REPOSITORY Specifies the default repository to download on init. 21 | # -DLPM_DEFAULT_RELEASE Specifies the default release for lpm for `self-upgrade`. If set to an empty string, will disable the ability to self-upgrade. This should be done for lpm builds for system package managers. 22 | # -DLPM_VERSION Specifies the lpm version. 23 | # -DLPM_ARCH_PROCESSOR Manually specifies the processor archiecture. 24 | # -DLPM_ARCH_PLATFORM Manually specifies the operating system. 25 | 26 | SRCS="src/*.c" 27 | COMPILE_FLAGS="$CFLAGS -I`pwd`/lib/prefix/include" # We specifically rename this and LDFLAGS, because exotic build environments export these to subprocesses. 28 | LINK_FLAGS="$LDFLAGS -lm -L`pwd`/lib/prefix/lib -L`pwd`/lib/prefix/lib64" # And ideally we don't want to mess with the underlying build processes, unless we're explicit about it. 29 | 30 | [[ "$@" == "clean" ]] && rm -rf lib/libgit2/build lib/zlib/build lib/libzip/build lib/xz/build lib/mbedtls/build lib/prefix lua $BIN *.exe src/lpm.luac src/lpm.lua.c && exit 0 31 | cmake --version >/dev/null 2>/dev/null || { echo "Please ensure that you have cmake installed." && exit -1; } 32 | 33 | # Build supporting libraries, libz, liblzma, libmbedtls, libmbedcrypto, libgit2, libzip, libmicrotar, liblua 34 | [[ " $@" != *" -g"* ]] && CMAKE_DEFAULT_FLAGS="$CMAKE_DEFAULT_FLAGS -DCMAKE_BUILD_TYPE=Release" || CMAKE_DEFUALT_FLAGS="$CMAKE_DEFAULT_FLAGS -DCMAKE_BUILD_TYPE=Debug" 35 | if [[ " $@" != *" -O"* ]]; then 36 | [[ " $@" != *" -g"* ]] && COMPILE_FLAGS="$COMPILE_FLAGS -O3" || COMPILE_FLAGS="$COMPILE_FLAGS -O0" 37 | fi 38 | CMAKE_DEFAULT_FLAGS=" $CMAKE_DEFAULT_FLAGS -DCMAKE_PREFIX_PATH=`pwd`/lib/prefix -DCMAKE_INSTALL_PREFIX=`pwd`/lib/prefix -DBUILD_SHARED_LIBS=OFF" 39 | mkdir -p lib/prefix/include lib/prefix/lib 40 | if [[ "$@" != *"-lz"* ]]; then 41 | [ ! -e "lib/zlib" ] && echo "Make sure you've cloned submodules. (git submodule update --init --depth=1)" && exit -1 42 | [[ ! -e "lib/zlib/build" ]] && { cd lib/zlib && mkdir build && cd build && $CC $COMPILE_FLAGS -D_LARGEFILE64_SOURCE -I.. ../*.c -c && $AR rc libz.a *.o && cp libz.a ../../prefix/lib && cp ../*.h ../../prefix/include && cd ../../../ || exit -1; } 43 | LINK_FLAGS="$LINK_FLAGS -lz" 44 | fi 45 | if [[ "$@" != *"-llzma"* ]]; then 46 | [ ! -e "lib/xz/build" ] && { cd lib/xz && mkdir build && cd build && CFLAGS="$COMPILE_FLAGS -Wno-incompatible-pointer-types" cmake .. -G "Unix Makefiles" $CMAKE_DEFAULT_FLAGS -DHAVE_ENCODERS=false && $MAKE -j $JOBS && $MAKE install && cd ../../../ || exit -1; } 47 | LINK_FLAGS="$LINK_FLAGS -llzma" 48 | fi 49 | if [[ "$@" != *"-DLPM_NO_NETWORK"* && "$@" != *"-lmbedtls"* && "$@" != *"-lmbedcrypto"* && "$@" != *"-lmbedx509"* ]]; then 50 | [ ! -e "lib/mbedtls/build" ] && { cd lib/mbedtls && mkdir build && cd build && CFLAGS="$COMPILE_FLAGS $CFLAGS_MBEDTLS -DMBEDTLS_MD4_C=1 -DMBEDTLS_DEBUG_C -w" cmake .. $CMAKE_DEFAULT_FLAGS -G "Unix Makefiles" -DENABLE_TESTING=OFF -DENABLE_PROGRAMS=OFF $SSL_CONFIGURE && CFLAGS="$COMPILE_FLAGS $CFLAGS_MBEDTLS -DMBEDTLS_MD4_C=1 -w" $MAKE -j $JOBS && $MAKE install && cd ../../../ || exit -1; } 51 | LINK_FLAGS="$LINK_FLAGS -lmbedtls -lmbedx509 -lmbedcrypto" 52 | fi 53 | if [[ "$@" != *"-DLPM_NO_GIT"* && "$@" != *"-lgit2"* ]]; then 54 | [[ "$@" != *"-DLPM_NO_NETWORK"* ]] && USE_HTTPS="mbedTLS" || USE_HTTPS="OFF" 55 | [ ! -e "lib/libgit2/build" ] && { cd lib/libgit2 && mkdir build && cd build && cmake .. -G "Unix Makefiles" $GIT2_CONFIGURE $CMAKE_DEFAULT_FLAGS -DCMAKE_BUILD_TYPE=MinSizeRel -DCMAKE_C_STANDARD=99 -DBUILD_TESTS=OFF -DBUILD_CLI=OFF -DBUILD_SHARED_LIBS=OFF -DREGEX_BACKEND=builtin -DUSE_SSH=OFF -DUSE_HTTPS=$USE_HTTPS && $MAKE -j $JOBS && $MAKE install && cd ../../../ || exit -1; } 56 | LINK_FLAGS="-lgit2 $LINK_FLAGS" 57 | fi 58 | if [[ "$@" != *"-lzip"* ]]; then 59 | [ ! -e "lib/libzip/build" ] && { cd lib/libzip && mkdir build && cd build && CFLAGS="$COMPILE_FLAGS -Wno-incompatible-pointer-types" cmake .. -G "Unix Makefiles" $CMAKE_DEFAULT_FLAGS -DBUILD_TOOLS=OFF -DBUILD_EXAMPLES=OFF -DBUILD_DOC=OFF -DENABLE_COMMONCRYPTO=OFF -DENABLE_GNUTLS=OFF -DENABLE_OPENSSL=OFF -DENABLE_BZIP2=OFF -DENABLE_LZMA=ON -DENABLE_ZSTD=OFF && $MAKE -j $JOBS && $MAKE install && cd ../../../ || exit -1; } 60 | LINK_FLAGS="$LINK_FLAGS -lzip -llzma" 61 | fi 62 | [[ "$@" != *"-lmicrotar"* ]] && COMPILE_FLAGS="$COMPILE_FLAGS -Ilib/microtar/src" && SRCS="$SRCS lib/microtar/src/microtar.c" 63 | [[ "$@" != *"-llua"* ]] && COMPILE_FLAGS="$COMPILE_FLAGS -Ilib/lua -DMAKE_LIB=1" && SRCS="$SRCS lib/lua/onelua.c" 64 | 65 | # Build the pre-packaged lua file into the executbale. 66 | if [[ "$@" == *"-DLPM_STATIC"* ]]; then 67 | [[ ! -e "lua.exe" ]] && { $HOSTCC -Ilib/lua -o lua.exe lib/lua/onelua.c -lm || exit -1; } 68 | ./lua.exe -e 'io.open("src/lpm.lua.c", "wb"):write("unsigned char lpm_luac[] = \""..string.dump(load(io.lines("src/lpm.lua","L"), "=lpm.lua")):gsub(".",function(c) return string.format("\\x%02X",string.byte(c)) end).."\";unsigned int lpm_luac_len = sizeof(lpm_luac)-1;")' 69 | fi 70 | 71 | [[ $OSTYPE != 'msys'* && $OSTYPE != 'cygwin'* && $CC != *'mingw'* && $CC != "emcc" ]] && COMPILE_FLAGS="$COMPILE_FLAGS -DLUA_USE_LINUX" && LINK_FLAGS="$LINK_FLAGS -ldl" 72 | [[ $OSTYPE == 'msys'* || $OSTYPE == 'cygwin'* || $CC == *'mingw'* ]] && LINK_FLAGS="$LINK_FLAGS -lbcrypt -lz -lole32 -lcrypt32 -lrpcrt4 -lsecur32" 73 | [[ $OSTYPE == 'msys'* || $OSTYPE == 'cygwin'* || $CC == *'mingw'* && "$@" != *"-DLPM_NO_NETWORK"* ]] && LINK_FLAGS="$LINK_FLAGS -lws2_32 -lwinhttp" 74 | [[ $OSTYPE == *'darwin'* ]] && LINK_FLAGS="$LINK_FLAGS -liconv -framework Foundation" 75 | [[ $OSTYPE == *'darwin'* ]] && "$@" != *"-DLPM_NO_NETWORK"* && LINK_FLAGS="$LINK_FLAGS -framework Security" 76 | 77 | [[ " $@" != *" -g"* && " $@" != *" -O"* ]] && LINK_FLAGS="$LINK_FLAGS -s -flto" 78 | $CC $COMPILE_FLAGS $SRCS $@ -o $BIN $LINK_FLAGS 79 | -------------------------------------------------------------------------------- /lib/microtar/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 rxi 2 | Copyright (c) 2024 Gaspartcho 3 | 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/microtar/README.md: -------------------------------------------------------------------------------- 1 | # microtar (a fork) 2 | A lightweight tar library written in ANSI C 3 | Initially forked from https://github.com/rxi/microtar 4 | 5 | 6 | ## Basic Usage 7 | The library consists of `microtar.c` and `microtar.h`. These two files can be 8 | dropped into an existing project and compiled along with it. 9 | 10 | 11 | #### Reading 12 | ```c 13 | mtar_t tar; 14 | mtar_header_t h; 15 | char *p; 16 | 17 | /* Open archive for reading */ 18 | mtar_open(&tar, "test.tar", "r"); 19 | 20 | /* Print all file names and sizes */ 21 | while ( (mtar_read_header(&tar, &h)) != MTAR_ENULLRECORD ) { 22 | printf("%s (%d bytes)\n", h.name, h.size); 23 | mtar_next(&tar); 24 | } 25 | 26 | /* Load and print contents of file "test.txt" */ 27 | mtar_find(&tar, "test.txt", &h); 28 | p = calloc(1, h.size + 1); 29 | mtar_read_data(&tar, p, h.size); 30 | printf("%s", p); 31 | free(p); 32 | 33 | /* Close archive */ 34 | mtar_close(&tar); 35 | ``` 36 | 37 | #### Writing 38 | ```c 39 | mtar_t tar; 40 | const char *str1 = "Hello world"; 41 | const char *str2 = "Goodbye world"; 42 | 43 | /* Open archive for writing */ 44 | mtar_open(&tar, "test.tar", "w"); 45 | 46 | /* Write strings to files `test1.txt` and `test2.txt` */ 47 | mtar_write_file_header(&tar, "test1.txt", strlen(str1)); 48 | mtar_write_data(&tar, str1, strlen(str1)); 49 | mtar_write_file_header(&tar, "test2.txt", strlen(str2)); 50 | mtar_write_data(&tar, str2, strlen(str2)); 51 | 52 | /* Finalize -- this needs to be the last thing done before closing */ 53 | mtar_finalize(&tar); 54 | 55 | /* Close archive */ 56 | mtar_close(&tar); 57 | ``` 58 | 59 | 60 | ## Error handling 61 | All functions which return an `int` will return `MTAR_ESUCCESS` if the operation 62 | is successful. If an error occurs an error value less-than-zero will be 63 | returned; this value can be passed to the function `mtar_strerror()` to get its 64 | corresponding error string. 65 | 66 | 67 | ## Wrapping a stream 68 | If you want to read or write from something other than a file, the `mtar_t` 69 | struct can be manually initialized with your own callback functions and a 70 | `stream` pointer. 71 | 72 | All callback functions are passed a pointer to the `mtar_t` struct as their 73 | first argument. They should return `MTAR_ESUCCESS` if the operation succeeds 74 | without an error, or an integer below zero if an error occurs. 75 | 76 | After the `stream` field has been set, all required callbacks have been set and 77 | all unused fields have been zeroset the `mtar_t` struct can be safely used with 78 | the microtar functions. `mtar_open` *should not* be called if the `mtar_t` 79 | struct was initialized manually. 80 | 81 | #### Reading 82 | The following callbacks should be set for reading an archive from a stream: 83 | 84 | Name | Arguments | Description 85 | --------|------------------------------------------|--------------------------- 86 | `read` | `mtar_t *tar, void *data, unsigned size` | Read data from the stream 87 | `seek` | `mtar_t *tar, unsigned pos` | Set the position indicator 88 | `close` | `mtar_t *tar` | Close the stream 89 | 90 | #### Writing 91 | The following callbacks should be set for writing an archive to a stream: 92 | 93 | Name | Arguments | Description 94 | --------|------------------------------------------------|--------------------- 95 | `write` | `mtar_t *tar, const void *data, unsigned size` | Write data to the stream 96 | 97 | 98 | ## License 99 | This library is free software; you can redistribute it and/or modify it under 100 | the terms of the MIT license. See [LICENSE](LICENSE) for details. 101 | -------------------------------------------------------------------------------- /lib/microtar/src/microtar.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 rxi 3 | * Copyright (c) 2024 Gaspartcho 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to 7 | * deal in the Software without restriction, including without limitation the 8 | * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | * sell copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in 13 | * all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | * IN THE SOFTWARE. 22 | */ 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #include "microtar.h" 30 | 31 | typedef struct { 32 | // Original tar header fields 33 | char file_path[100]; 34 | char file_mode[8]; 35 | char owner_user_id[8]; 36 | char owner_group_id[8]; 37 | char file_size[12]; 38 | char file_mtime[12]; 39 | char header_checksum[8]; 40 | char file_type; 41 | char link_path[100]; 42 | 43 | // New UStar fields 44 | char magic_bytes[6]; 45 | char version[2]; 46 | char owner_user_name[32]; 47 | char owner_group_name[32]; 48 | char device_major_number[8]; 49 | char device_minor_number[8]; 50 | char prefix[155]; 51 | 52 | char padding[12]; 53 | } mtar_raw_header_t; 54 | 55 | 56 | static unsigned round_up(unsigned n, unsigned incr) { 57 | return n + (incr - n % incr) % incr; 58 | } 59 | 60 | 61 | static unsigned checksum(const mtar_raw_header_t* rh) { 62 | unsigned i; 63 | unsigned char *p = (unsigned char*) rh; 64 | unsigned res = 256; 65 | for (i = 0; i < offsetof(mtar_raw_header_t, header_checksum); i++) { 66 | res += p[i]; 67 | } 68 | for (i = offsetof(mtar_raw_header_t, file_type); i < sizeof(*rh); i++) { 69 | res += p[i]; 70 | } 71 | return res; 72 | } 73 | 74 | 75 | static int tread(mtar_t *tar, void *data, unsigned size) { 76 | int err = tar->read(tar, data, size); 77 | tar->pos += size; 78 | return err; 79 | } 80 | 81 | 82 | static int twrite(mtar_t *tar, const void *data, unsigned size) { 83 | int err = tar->write(tar, data, size); 84 | tar->pos += size; 85 | return err; 86 | } 87 | 88 | 89 | static int write_null_bytes(mtar_t *tar, int n) { 90 | int i, err; 91 | char nul = '\0'; 92 | for (i = 0; i < n; i++) { 93 | err = twrite(tar, &nul, 1); 94 | if (err) { 95 | return err; 96 | } 97 | } 98 | return MTAR_ESUCCESS; 99 | } 100 | 101 | static int read_filename(char *dest, const char *name, const char *prefix) { 102 | // If there's no prefix, use name directly 103 | if (prefix[0] == '\0') { 104 | strcpy(dest, name); 105 | dest[100] = '\0'; 106 | return MTAR_ESUCCESS; 107 | } 108 | 109 | // If there is a prefix, the path is: '/' 110 | strcpy(dest, prefix); 111 | strcat(dest, "/"); 112 | strcat(dest, name); 113 | dest[256] = '\0'; 114 | 115 | return MTAR_ESUCCESS; 116 | } 117 | 118 | 119 | static int raw_to_header(mtar_header_t *h, const mtar_raw_header_t *rh) { 120 | unsigned chksum1, chksum2; 121 | 122 | /* If the checksum starts with a null byte we assume the record is NULL */ 123 | if (*rh->header_checksum == '\0') { 124 | return MTAR_ENULLRECORD; 125 | } 126 | 127 | /* Build and compare checksum */ 128 | chksum1 = checksum(rh); 129 | sscanf(rh->header_checksum, "%o", &chksum2); 130 | if (chksum1 != chksum2) { 131 | return MTAR_EBADCHKSUM; 132 | } 133 | 134 | /* Load raw header into header */ 135 | sscanf(rh->file_mode, "%o", &h->mode); 136 | sscanf(rh->owner_user_id, "%o", &h->owner); 137 | sscanf(rh->file_size, "%o", &h->size); 138 | sscanf(rh->file_mtime, "%o", &h->mtime); 139 | h->type = rh->file_type; 140 | read_filename(h->name, rh->file_path, rh->prefix); 141 | read_filename(h->linkname, rh->link_path, rh->prefix); 142 | 143 | return MTAR_ESUCCESS; 144 | } 145 | 146 | 147 | static int header_to_raw(mtar_raw_header_t *rh, const mtar_header_t *h) { 148 | unsigned chksum; 149 | 150 | /* Load header into raw header */ 151 | memset(rh, 0, sizeof(*rh)); 152 | sprintf(rh->file_mode , "%o", h->mode); 153 | sprintf(rh->owner_user_id, "%o", h->owner); 154 | sprintf(rh->file_size, "%o", h->size); 155 | sprintf(rh->file_mtime, "%o", h->mtime); 156 | rh->file_type = h->type ? h->type : MTAR_TREG; 157 | strcpy(rh->file_path, h->name); 158 | strcpy(rh->link_path, h->linkname); 159 | 160 | 161 | /* Calculate and write checksum */ 162 | chksum = checksum(rh); 163 | sprintf(rh->header_checksum, "%06o", chksum); 164 | rh->header_checksum[7] = ' '; 165 | 166 | return MTAR_ESUCCESS; 167 | } 168 | 169 | 170 | const char* mtar_strerror(int err) { 171 | switch (err) { 172 | case MTAR_ESUCCESS : return "success"; 173 | case MTAR_EFAILURE : return "failure"; 174 | case MTAR_EOPENFAIL : return "could not open"; 175 | case MTAR_EREADFAIL : return "could not read"; 176 | case MTAR_EWRITEFAIL : return "could not write"; 177 | case MTAR_ESEEKFAIL : return "could not seek"; 178 | case MTAR_EBADCHKSUM : return "bad checksum"; 179 | case MTAR_ENULLRECORD : return "null record"; 180 | case MTAR_ENOTFOUND : return "file not found"; 181 | } 182 | return "unknown error"; 183 | } 184 | 185 | 186 | static int file_write(mtar_t *tar, const void *data, unsigned size) { 187 | unsigned res = fwrite(data, 1, size, tar->stream); 188 | return (res == size) ? MTAR_ESUCCESS : MTAR_EWRITEFAIL; 189 | } 190 | 191 | static int file_read(mtar_t *tar, void *data, unsigned size) { 192 | unsigned res = fread(data, 1, size, tar->stream); 193 | return (res == size) ? MTAR_ESUCCESS : MTAR_EREADFAIL; 194 | } 195 | 196 | static int file_seek(mtar_t *tar, unsigned offset) { 197 | int res = fseek(tar->stream, offset, SEEK_SET); 198 | return (res == 0) ? MTAR_ESUCCESS : MTAR_ESEEKFAIL; 199 | } 200 | 201 | static int file_close(mtar_t *tar) { 202 | fclose(tar->stream); 203 | return MTAR_ESUCCESS; 204 | } 205 | 206 | 207 | int mtar_open(mtar_t *tar, const char *filename, const char *mode) { 208 | int err; 209 | mtar_header_t h; 210 | 211 | /* Init tar struct and functions */ 212 | memset(tar, 0, sizeof(*tar)); 213 | tar->write = file_write; 214 | tar->read = file_read; 215 | tar->seek = file_seek; 216 | tar->close = file_close; 217 | 218 | /* Assure mode is always binary */ 219 | if ( strchr(mode, 'r') ) mode = "rb"; 220 | if ( strchr(mode, 'w') ) mode = "wb"; 221 | if ( strchr(mode, 'a') ) mode = "ab"; 222 | /* Open file */ 223 | tar->stream = fopen(filename, mode); 224 | if (!tar->stream) { 225 | return MTAR_EOPENFAIL; 226 | } 227 | /* Read first header to check it is valid if mode is `r` */ 228 | if (*mode == 'r') { 229 | err = mtar_read_header(tar, &h); 230 | if (err != MTAR_ESUCCESS) { 231 | mtar_close(tar); 232 | return err; 233 | } 234 | } 235 | 236 | /* Return ok */ 237 | return MTAR_ESUCCESS; 238 | } 239 | 240 | 241 | int mtar_close(mtar_t *tar) { 242 | return tar->close(tar); 243 | } 244 | 245 | 246 | int mtar_seek(mtar_t *tar, unsigned pos) { 247 | int err = tar->seek(tar, pos); 248 | tar->pos = pos; 249 | return err; 250 | } 251 | 252 | 253 | int mtar_rewind(mtar_t *tar) { 254 | tar->remaining_data = 0; 255 | tar->last_header = 0; 256 | return mtar_seek(tar, 0); 257 | } 258 | 259 | 260 | int mtar_next(mtar_t *tar) { 261 | int err, n; 262 | mtar_header_t h; 263 | /* Load header */ 264 | err = mtar_read_header(tar, &h); 265 | if (err) { 266 | return err; 267 | } 268 | /* Seek to next record */ 269 | n = round_up(h.size, 512) + sizeof(mtar_raw_header_t); 270 | return mtar_seek(tar, tar->pos + n); 271 | } 272 | 273 | 274 | int mtar_find(mtar_t *tar, const char *name, mtar_header_t *h) { 275 | int err; 276 | mtar_header_t header; 277 | /* Start at beginning */ 278 | err = mtar_rewind(tar); 279 | if (err) { 280 | return err; 281 | } 282 | /* Iterate all files until we hit an error or find the file */ 283 | while ( (err = mtar_read_header(tar, &header)) == MTAR_ESUCCESS ) { 284 | if ( !strcmp(header.name, name) ) { 285 | if (h) { 286 | *h = header; 287 | } 288 | return MTAR_ESUCCESS; 289 | } 290 | mtar_next(tar); 291 | } 292 | /* Return error */ 293 | if (err == MTAR_ENULLRECORD) { 294 | err = MTAR_ENOTFOUND; 295 | } 296 | return err; 297 | } 298 | 299 | 300 | int mtar_read_header(mtar_t *tar, mtar_header_t *h) { 301 | int err; 302 | mtar_raw_header_t rh; 303 | /* Save header position */ 304 | tar->last_header = tar->pos; 305 | /* Read raw header */ 306 | err = tread(tar, &rh, sizeof(rh)); 307 | if (err) { 308 | return err; 309 | } 310 | /* Seek back to start of header */ 311 | err = mtar_seek(tar, tar->last_header); 312 | if (err) { 313 | return err; 314 | } 315 | /* Load raw header into header struct and return */ 316 | return raw_to_header(h, &rh); 317 | } 318 | 319 | 320 | int mtar_update_header(mtar_header_t *h, mtar_header_t *oh) { 321 | if (oh->mode) h->mode = oh->mode; 322 | if (oh->owner) h->owner = oh->owner; 323 | if (oh->size) h->size = oh->size; 324 | if (oh->mtime) h->mtime = oh->mtime; 325 | if (oh->type) h->type = oh->type; 326 | 327 | if (oh->name[0] != '\0') strcpy(h->name, oh->name); 328 | if (oh->linkname[0] != '\0') strcpy(h->linkname, oh->linkname); 329 | 330 | return MTAR_ESUCCESS; 331 | } 332 | 333 | 334 | int mtar_clear_header(mtar_header_t *h){ 335 | memset(h, 0, sizeof(mtar_header_t)); 336 | return MTAR_ESUCCESS; 337 | } 338 | 339 | 340 | int mtar_read_data(mtar_t *tar, void *ptr, unsigned size) { 341 | int err; 342 | /* If we have no remaining data then this is the first read, we get the size, 343 | * set the remaining data and seek to the beginning of the data */ 344 | if (tar->remaining_data == 0) { 345 | mtar_header_t h; 346 | /* Read header */ 347 | err = mtar_read_header(tar, &h); 348 | if (err) { 349 | return err; 350 | } 351 | /* Seek past header and init remaining data */ 352 | err = mtar_seek(tar, tar->pos + sizeof(mtar_raw_header_t)); 353 | if (err) { 354 | return err; 355 | } 356 | tar->remaining_data = h.size; 357 | } 358 | /* Read data */ 359 | err = tread(tar, ptr, size); 360 | if (err) { 361 | return err; 362 | } 363 | tar->remaining_data -= size; 364 | /* If there is no remaining data we've finished reading and seek back to the 365 | * header */ 366 | if (tar->remaining_data == 0) { 367 | return mtar_seek(tar, tar->last_header); 368 | } 369 | return MTAR_ESUCCESS; 370 | } 371 | 372 | 373 | int mtar_write_header(mtar_t *tar, const mtar_header_t *h) { 374 | mtar_raw_header_t rh; 375 | /* Build raw header and write */ 376 | header_to_raw(&rh, h); 377 | tar->remaining_data = h->size; 378 | return twrite(tar, &rh, sizeof(rh)); 379 | } 380 | 381 | 382 | int mtar_write_file_header(mtar_t *tar, const char *name, unsigned size) { 383 | mtar_header_t h; 384 | /* Build header */ 385 | memset(&h, 0, sizeof(h)); 386 | strcpy(h.name, name); 387 | h.size = size; 388 | h.type = MTAR_TREG; 389 | h.mode = 0664; 390 | /* Write header */ 391 | return mtar_write_header(tar, &h); 392 | } 393 | 394 | 395 | int mtar_write_dir_header(mtar_t *tar, const char *name) { 396 | mtar_header_t h; 397 | /* Build header */ 398 | memset(&h, 0, sizeof(h)); 399 | strcpy(h.name, name); 400 | h.type = MTAR_TDIR; 401 | h.mode = 0775; 402 | /* Write header */ 403 | return mtar_write_header(tar, &h); 404 | } 405 | 406 | 407 | int mtar_write_data(mtar_t *tar, const void *data, unsigned size) { 408 | int err; 409 | /* Write data */ 410 | err = twrite(tar, data, size); 411 | if (err) { 412 | return err; 413 | } 414 | tar->remaining_data -= size; 415 | /* Write padding if we've written all the data for this file */ 416 | if (tar->remaining_data == 0) { 417 | return write_null_bytes(tar, round_up(tar->pos, 512) - tar->pos); 418 | } 419 | return MTAR_ESUCCESS; 420 | } 421 | 422 | 423 | int mtar_finalize(mtar_t *tar) { 424 | /* Write two NULL records */ 425 | return write_null_bytes(tar, sizeof(mtar_raw_header_t) * 2); 426 | } 427 | 428 | -------------------------------------------------------------------------------- /lib/microtar/src/microtar.h: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Copyright (c) 2017 rxi 4 | * Copyright (c) 2024 Gaspartcho 5 | * 6 | * This library is free software; you can redistribute it and/or modify it 7 | * under the terms of the MIT license. See `microtar.c` for details. 8 | */ 9 | 10 | #ifndef MICROTAR_H 11 | #define MICROTAR_H 12 | 13 | #ifdef __cplusplus 14 | extern "C" 15 | { 16 | #endif 17 | 18 | #include 19 | #include 20 | 21 | #define MTAR_VERSION "0.1.0" 22 | 23 | enum { 24 | MTAR_ESUCCESS = 0, 25 | MTAR_EFAILURE = -1, 26 | MTAR_EOPENFAIL = -2, 27 | MTAR_EREADFAIL = -3, 28 | MTAR_EWRITEFAIL = -4, 29 | MTAR_ESEEKFAIL = -5, 30 | MTAR_EBADCHKSUM = -6, 31 | MTAR_ENULLRECORD = -7, 32 | MTAR_ENOTFOUND = -8 33 | }; 34 | 35 | enum { 36 | MTAR_TREG = '0', 37 | MTAR_TLNK = '1', 38 | MTAR_TSYM = '2', 39 | MTAR_TCHR = '3', 40 | MTAR_TBLK = '4', 41 | MTAR_TDIR = '5', 42 | MTAR_TFIFO = '6', 43 | MTAR_TCON = '7', 44 | MTAR_TEHR = 'x', // PAX file format 45 | MTAR_TEHRA = 'g', // PAX file format 46 | MTAR_TGLP = 'K', // GNU file format 47 | MTAR_TGFP = 'L' // GNU file format 48 | }; 49 | 50 | typedef struct { 51 | unsigned mode; 52 | unsigned owner; 53 | unsigned size; 54 | unsigned mtime; 55 | unsigned type; 56 | char name[4096]; 57 | char linkname[4096]; 58 | } mtar_header_t; 59 | 60 | 61 | typedef struct mtar_t mtar_t; 62 | 63 | struct mtar_t { 64 | int (*read)(mtar_t *tar, void *data, unsigned size); 65 | int (*write)(mtar_t *tar, const void *data, unsigned size); 66 | int (*seek)(mtar_t *tar, unsigned pos); 67 | int (*close)(mtar_t *tar); 68 | void *stream; 69 | unsigned pos; 70 | unsigned remaining_data; 71 | unsigned last_header; 72 | }; 73 | 74 | 75 | const char* mtar_strerror(int err); 76 | 77 | int mtar_open(mtar_t *tar, const char *filename, const char *mode); 78 | int mtar_close(mtar_t *tar); 79 | 80 | int mtar_seek(mtar_t *tar, unsigned pos); 81 | int mtar_rewind(mtar_t *tar); 82 | int mtar_next(mtar_t *tar); 83 | int mtar_find(mtar_t *tar, const char *name, mtar_header_t *h); 84 | int mtar_read_header(mtar_t *tar, mtar_header_t *h); 85 | int mtar_update_header(mtar_header_t *h, mtar_header_t *oh); 86 | int mtar_clear_header(mtar_header_t *h); 87 | int mtar_read_data(mtar_t *tar, void *ptr, unsigned size); 88 | 89 | int mtar_write_header(mtar_t *tar, const mtar_header_t *h); 90 | int mtar_write_file_header(mtar_t *tar, const char *name, unsigned size); 91 | int mtar_write_dir_header(mtar_t *tar, const char *name); 92 | int mtar_write_data(mtar_t *tar, const void *data, unsigned size); 93 | int mtar_finalize(mtar_t *tar); 94 | 95 | #ifdef __cplusplus 96 | } 97 | #endif 98 | 99 | #endif 100 | 101 | -------------------------------------------------------------------------------- /libraries/json.lua: -------------------------------------------------------------------------------- 1 | -- mod-version:3 --lite-xl 2.1 2 | -- 3 | -- json.lua 4 | -- 5 | -- Copyright (c) 2020 rxi 6 | -- 7 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | -- this software and associated documentation files (the "Software"), to deal in 9 | -- the Software without restriction, including without limitation the rights to 10 | -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 11 | -- of the Software, and to permit persons to whom the Software is furnished to do 12 | -- so, subject to the following conditions: 13 | -- 14 | -- The above copyright notice and this permission notice shall be included in all 15 | -- copies or substantial portions of the Software. 16 | -- 17 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | -- SOFTWARE. 24 | -- 25 | 26 | local json = { _version = "0.1.2" } 27 | local status, cjson = pcall(require, "libraries.cjson") 28 | if status then 29 | return cjson 30 | end 31 | 32 | ------------------------------------------------------------------------------- 33 | -- Encode 34 | ------------------------------------------------------------------------------- 35 | 36 | local encode 37 | 38 | local escape_char_map = { 39 | [ "\\" ] = "\\", 40 | [ "\"" ] = "\"", 41 | [ "\b" ] = "b", 42 | [ "\f" ] = "f", 43 | [ "\n" ] = "n", 44 | [ "\r" ] = "r", 45 | [ "\t" ] = "t", 46 | } 47 | 48 | local escape_char_map_inv = { [ "/" ] = "/" } 49 | for k, v in pairs(escape_char_map) do 50 | escape_char_map_inv[v] = k 51 | end 52 | 53 | 54 | local function escape_char(c) 55 | return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) 56 | end 57 | 58 | 59 | local function encode_nil(val) 60 | return "null" 61 | end 62 | 63 | 64 | local function encode_table(val, stack) 65 | local res = {} 66 | stack = stack or {} 67 | 68 | -- Circular reference? 69 | if stack[val] then error("circular reference") end 70 | 71 | stack[val] = true 72 | 73 | if rawget(val, 1) ~= nil or next(val) == nil then 74 | -- Treat as array -- check keys are valid and it is not sparse 75 | local n = 0 76 | for k in pairs(val) do 77 | if type(k) ~= "number" then 78 | error("invalid table: mixed or invalid key types") 79 | end 80 | n = n + 1 81 | end 82 | if n ~= #val then 83 | error("invalid table: sparse array") 84 | end 85 | -- Encode 86 | for i, v in ipairs(val) do 87 | table.insert(res, encode(v, stack)) 88 | end 89 | stack[val] = nil 90 | return "[" .. table.concat(res, ",") .. "]" 91 | 92 | else 93 | -- Treat as an object 94 | for k, v in pairs(val) do 95 | if type(k) ~= "string" then 96 | error("invalid table: mixed or invalid key types") 97 | end 98 | table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) 99 | end 100 | stack[val] = nil 101 | return "{" .. table.concat(res, ",") .. "}" 102 | end 103 | end 104 | 105 | 106 | local function encode_string(val) 107 | return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' 108 | end 109 | 110 | 111 | local function encode_number(val) 112 | -- Check for NaN, -inf and inf 113 | if val ~= val or val <= -math.huge or val >= math.huge then 114 | error("unexpected number value '" .. tostring(val) .. "'") 115 | end 116 | return string.format("%.14g", val) 117 | end 118 | 119 | 120 | local type_func_map = { 121 | [ "nil" ] = encode_nil, 122 | [ "table" ] = encode_table, 123 | [ "string" ] = encode_string, 124 | [ "number" ] = encode_number, 125 | [ "boolean" ] = tostring, 126 | } 127 | 128 | 129 | encode = function(val, stack) 130 | local t = type(val) 131 | local f = type_func_map[t] 132 | if f then 133 | return f(val, stack) 134 | end 135 | error("unexpected type '" .. t .. "'") 136 | end 137 | 138 | 139 | function json.encode(val) 140 | return ( encode(val) ) 141 | end 142 | 143 | 144 | ------------------------------------------------------------------------------- 145 | -- Decode 146 | ------------------------------------------------------------------------------- 147 | 148 | local parse 149 | 150 | local function create_set(...) 151 | local res = {} 152 | for i = 1, select("#", ...) do 153 | res[ select(i, ...) ] = true 154 | end 155 | return res 156 | end 157 | 158 | local space_chars = create_set(" ", "\t", "\r", "\n") 159 | local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") 160 | local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") 161 | local literals = create_set("true", "false", "null") 162 | 163 | local literal_map = { 164 | [ "true" ] = true, 165 | [ "false" ] = false, 166 | [ "null" ] = nil, 167 | } 168 | 169 | 170 | local function next_char(str, idx, set, negate) 171 | for i = idx, #str do 172 | if set[str:sub(i, i)] ~= negate then 173 | return i 174 | end 175 | end 176 | return #str + 1 177 | end 178 | 179 | 180 | local function decode_error(str, idx, msg) 181 | local line_count = 1 182 | local col_count = 1 183 | for i = 1, idx - 1 do 184 | col_count = col_count + 1 185 | if str:sub(i, i) == "\n" then 186 | line_count = line_count + 1 187 | col_count = 1 188 | end 189 | end 190 | error( string.format("%s at line %d col %d", msg, line_count, col_count) ) 191 | end 192 | 193 | 194 | local function codepoint_to_utf8(n) 195 | -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa 196 | local f = math.floor 197 | if n <= 0x7f then 198 | return string.char(n) 199 | elseif n <= 0x7ff then 200 | return string.char(f(n / 64) + 192, n % 64 + 128) 201 | elseif n <= 0xffff then 202 | return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) 203 | elseif n <= 0x10ffff then 204 | return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, 205 | f(n % 4096 / 64) + 128, n % 64 + 128) 206 | end 207 | error( string.format("invalid unicode codepoint '%x'", n) ) 208 | end 209 | 210 | 211 | local function parse_unicode_escape(s) 212 | local n1 = tonumber( s:sub(1, 4), 16 ) 213 | local n2 = tonumber( s:sub(7, 10), 16 ) 214 | -- Surrogate pair? 215 | if n2 then 216 | return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) 217 | else 218 | return codepoint_to_utf8(n1) 219 | end 220 | end 221 | 222 | 223 | local function parse_string(str, i) 224 | local res = "" 225 | local j = i + 1 226 | local k = j 227 | 228 | while j <= #str do 229 | local x = str:byte(j) 230 | 231 | if x < 32 then 232 | decode_error(str, j, "control character in string") 233 | 234 | elseif x == 92 then -- `\`: Escape 235 | res = res .. str:sub(k, j - 1) 236 | j = j + 1 237 | local c = str:sub(j, j) 238 | if c == "u" then 239 | local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) 240 | or str:match("^%x%x%x%x", j + 1) 241 | or decode_error(str, j - 1, "invalid unicode escape in string") 242 | res = res .. parse_unicode_escape(hex) 243 | j = j + #hex 244 | else 245 | if not escape_chars[c] then 246 | decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") 247 | end 248 | res = res .. escape_char_map_inv[c] 249 | end 250 | k = j + 1 251 | 252 | elseif x == 34 then -- `"`: End of string 253 | res = res .. str:sub(k, j - 1) 254 | return res, j + 1 255 | end 256 | 257 | j = j + 1 258 | end 259 | 260 | decode_error(str, i, "expected closing quote for string") 261 | end 262 | 263 | 264 | local function parse_number(str, i) 265 | local x = next_char(str, i, delim_chars) 266 | local s = str:sub(i, x - 1) 267 | local n = tonumber(s) 268 | if not n then 269 | decode_error(str, i, "invalid number '" .. s .. "'") 270 | end 271 | return n, x 272 | end 273 | 274 | 275 | local function parse_literal(str, i) 276 | local x = next_char(str, i, delim_chars) 277 | local word = str:sub(i, x - 1) 278 | if not literals[word] then 279 | decode_error(str, i, "invalid literal '" .. word .. "'") 280 | end 281 | return literal_map[word], x 282 | end 283 | 284 | 285 | local function parse_array(str, i) 286 | local res = {} 287 | local n = 1 288 | i = i + 1 289 | while 1 do 290 | local x 291 | i = next_char(str, i, space_chars, true) 292 | -- Empty / end of array? 293 | if str:sub(i, i) == "]" then 294 | i = i + 1 295 | break 296 | end 297 | -- Read token 298 | x, i = parse(str, i) 299 | res[n] = x 300 | n = n + 1 301 | -- Next token 302 | i = next_char(str, i, space_chars, true) 303 | local chr = str:sub(i, i) 304 | i = i + 1 305 | if chr == "]" then break end 306 | if chr ~= "," then decode_error(str, i, "expected ']' or ','") end 307 | end 308 | return res, i 309 | end 310 | 311 | 312 | local function parse_object(str, i) 313 | local res = {} 314 | i = i + 1 315 | while 1 do 316 | local key, val 317 | i = next_char(str, i, space_chars, true) 318 | -- Empty / end of object? 319 | if str:sub(i, i) == "}" then 320 | i = i + 1 321 | break 322 | end 323 | -- Read key 324 | if str:sub(i, i) ~= '"' then 325 | decode_error(str, i, "expected string for key") 326 | end 327 | key, i = parse(str, i) 328 | -- Read ':' delimiter 329 | i = next_char(str, i, space_chars, true) 330 | if str:sub(i, i) ~= ":" then 331 | decode_error(str, i, "expected ':' after key") 332 | end 333 | i = next_char(str, i + 1, space_chars, true) 334 | -- Read value 335 | val, i = parse(str, i) 336 | -- Set 337 | res[key] = val 338 | -- Next token 339 | i = next_char(str, i, space_chars, true) 340 | local chr = str:sub(i, i) 341 | i = i + 1 342 | if chr == "}" then break end 343 | if chr ~= "," then decode_error(str, i, "expected '}' or ','") end 344 | end 345 | return res, i 346 | end 347 | 348 | 349 | local char_func_map = { 350 | [ '"' ] = parse_string, 351 | [ "0" ] = parse_number, 352 | [ "1" ] = parse_number, 353 | [ "2" ] = parse_number, 354 | [ "3" ] = parse_number, 355 | [ "4" ] = parse_number, 356 | [ "5" ] = parse_number, 357 | [ "6" ] = parse_number, 358 | [ "7" ] = parse_number, 359 | [ "8" ] = parse_number, 360 | [ "9" ] = parse_number, 361 | [ "-" ] = parse_number, 362 | [ "t" ] = parse_literal, 363 | [ "f" ] = parse_literal, 364 | [ "n" ] = parse_literal, 365 | [ "[" ] = parse_array, 366 | [ "{" ] = parse_object, 367 | } 368 | 369 | 370 | parse = function(str, idx) 371 | local chr = str:sub(idx, idx) 372 | local f = char_func_map[chr] 373 | if f then 374 | return f(str, idx) 375 | end 376 | decode_error(str, idx, "unexpected character '" .. chr .. "'") 377 | end 378 | 379 | 380 | function json.decode(str) 381 | if type(str) ~= "string" then 382 | error("expected argument of type string, got " .. type(str)) 383 | end 384 | local res, idx = parse(str, next_char(str, 1, space_chars, true)) 385 | idx = next_char(str, idx, space_chars, true) 386 | if idx <= #str then 387 | decode_error(str, idx, "trailing garbage") 388 | end 389 | return res 390 | end 391 | 392 | 393 | return json 394 | -------------------------------------------------------------------------------- /lite-xl-plugin-manager.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "latest", 3 | "description": "Plugin and package manager for Lite XL editor.", 4 | "homepage": "https://github.com/lite-xl/lite-xl-plugin-manager", 5 | "license": "LPM", 6 | "architecture": { 7 | "64bit": { 8 | "url": "https://github.com/lite-xl/lite-xl-plugin-manager/releases/download/latest/lpm.x86_64-windows.exe#/lpm.exe", 9 | "checksum": "SKIP" 10 | } 11 | }, 12 | "bin": "lpm.exe", 13 | "shortcuts": [ 14 | [ 15 | "lpm.exe", 16 | "Lite-XL Plugin Manager" 17 | ] 18 | ], 19 | "checkver": { 20 | "url": "https://github.com/lite-xl/lite-xl-plugin-manager/tags", 21 | "regex": "/releases/tag/(?:v|V)?([\\d.]+)" 22 | }, 23 | "autoupdate": { 24 | "architecture": { 25 | "64bit": { 26 | "url": "https://github.com/lite-xl/lite-xl-plugin-manager/releases/download/latest/lpm.x86_64-windows.exe" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft-07/schema", 3 | "$id": "https://github.com/lite-xl/lite-xl-plugin-manager/raw/master/lpm.json", 4 | "title": "Lite XL Plugin Manager Manifest", 5 | "description": "Description of Lite XL addons", 6 | "$comment": "TODO: Add our own uri format for :.", 7 | "definitions": { 8 | "addon-id": { 9 | "type": "string", 10 | "pattern": "^[a-z0-9\\-_]+$" 11 | }, 12 | "addon-version": { 13 | "type": "string", 14 | "$comment": "TODO: should we avoid limiting this to only 3 components? See lite-xl-lsp-servers -> lsp_json.", 15 | "pattern": "^[0-9]+(\\.[0-9]+){0,2}$" 16 | }, 17 | "addon-version-specifier": { 18 | "type": "string", 19 | "pattern": "^[><]?=?[0-9]+(\\.[0-9]+){0,2}$" 20 | }, 21 | "mod-version": { 22 | "oneOf": [ 23 | { 24 | "type": "string", 25 | "pattern": "^[0-9\\.]+$" 26 | }, 27 | { 28 | "type": "integer", 29 | "minimum": 0 30 | } 31 | ] 32 | }, 33 | "arch": { 34 | "description": "Lite XL/Clang architecture tuple that the file is relevant for", 35 | "type": "string", 36 | "examples": [ 37 | "x86_64-linux", 38 | "x86_64-windows", 39 | "x86_64-darwin", 40 | "aarch64-darwin" 41 | ] 42 | }, 43 | "checksum": { 44 | "type": "string", 45 | "oneOf": [ 46 | { 47 | "pattern": "^[a-fA-F0-9]{64}$" 48 | }, 49 | { 50 | "const": "SKIP" 51 | } 52 | ] 53 | }, 54 | "files": { 55 | "type": "array", 56 | "items": { 57 | "type": "object", 58 | "properties": { 59 | "url": { 60 | "type": "string", 61 | "format": "uri" 62 | }, 63 | "checksum": { 64 | "$ref": "#/definitions/checksum" 65 | }, 66 | "arch": { 67 | "description": "List of architectures this file is relevant for", 68 | "oneOf": [ 69 | { 70 | "type": "array", 71 | "items": { 72 | "$ref": "#/definitions/arch" 73 | } 74 | }, 75 | { 76 | "$ref": "#/definitions/arch" 77 | } 78 | ] 79 | }, 80 | "path": { 81 | "description": "The location to install this file inside the addon's directory", 82 | "type": "string" 83 | }, 84 | "optional": { 85 | "description": "Whether the file is an optional addition. If omitted, the file is assumed to be required.", 86 | "type": "boolean" 87 | } 88 | }, 89 | "required": [ 90 | "url", 91 | "checksum" 92 | ] 93 | } 94 | } 95 | }, 96 | "type": "object", 97 | "properties": { 98 | "remotes": { 99 | "description": "A simple array of string repository identifiers", 100 | "type": "array", 101 | "items": { 102 | "type": "string" 103 | } 104 | }, 105 | "addons": { 106 | "description": "A simple array of string repository identifiers", 107 | "type": "array", 108 | "items": { 109 | "type": "object", 110 | "properties": { 111 | "id": { 112 | "description": "The semantic id of the addon", 113 | "$ref": "#/definitions/addon-id" 114 | }, 115 | "version": { 116 | "description": "The semantic version of the addon", 117 | "$ref": "#/definitions/addon-version" 118 | }, 119 | "mod_version": { 120 | "description": "The mod_version this addon is compatible with", 121 | "$ref": "#/definitions/mod-version" 122 | }, 123 | "type": { 124 | "description": "Addon type", 125 | "type": "string", 126 | "default": "plugin", 127 | "enum": [ 128 | "plugin", 129 | "library", 130 | "color", 131 | "font", 132 | "meta" 133 | ] 134 | }, 135 | "name": { 136 | "description": "The name of the addon", 137 | "type": "string" 138 | }, 139 | "description": { 140 | "description": "The description of the addon", 141 | "type": "string" 142 | }, 143 | "provides": { 144 | "description": "Array of strings that are a shorthand of functionality this addon provides. Can be used as a dependency.", 145 | "type": "array", 146 | "items": { 147 | "$ref": "#/definitions/addon-id" 148 | } 149 | }, 150 | "replaces": { 151 | "description": "Array of ids that this plugin explicitly replaces. Will always prefer this plugin in place of those plugins, so long as version requirements are met.", 152 | "type": "array", 153 | "items": { 154 | "$ref": "#/definitions/addon-id" 155 | } 156 | }, 157 | "remote": { 158 | "description": "Public https git link where this addon is located", 159 | "type": "string" 160 | }, 161 | "dependencies": { 162 | "description": "Table of dependencies", 163 | "type": "object", 164 | "propertyNames": { 165 | "$ref": "#/definitions/addon-id" 166 | }, 167 | "patternProperties": { 168 | "": { 169 | "version": { 170 | "description": "Dependency version specifier", 171 | "$ref": "#/definitions/addon-version-specifier" 172 | }, 173 | "optional": { 174 | "description": "Determines whether the dependency is optional", 175 | "type": "boolean" 176 | } 177 | } 178 | } 179 | }, 180 | "conflicts": { 181 | "description": "Table of addons that conflict with the current one", 182 | "type": "object", 183 | "propertyNames": { 184 | "$ref": "#/definitions/addon-id" 185 | }, 186 | "$comment": "TODO: The spec says in the same format as `dependencies`, but I don't think this supports `optional`.", 187 | "patternProperties": { 188 | "": { 189 | "version": { 190 | "description": "Dependency version specifier", 191 | "$ref": "#/definitions/addon-version-specifier" 192 | } 193 | } 194 | } 195 | }, 196 | "tags": { 197 | "description": "Freeform tags that may describe attributes of the addon", 198 | "type": "array", 199 | "$comment": "TODO: Should we limit the tag format? For example to #/definitions/addon-id", 200 | "items": { 201 | "type": "string" 202 | } 203 | }, 204 | "path": { 205 | "description": "The path to the addon in the repository. If omitted, will only pull the files in files. To pull the whole repository, use \".\".", 206 | "type": "string" 207 | }, 208 | "arch": { 209 | "description": "List of architectures this plugin supports. If not present, and no `files` entry specifies arches, the plugin is valid for all architectures. Otherwise uses the arches specified in `files` entries.", 210 | "type": "array", 211 | "items": { 212 | "$ref": "#/definitions/arch" 213 | } 214 | }, 215 | "post": { 216 | "description": "Commands to run after the addon is installed", 217 | "oneOf": [ 218 | { 219 | "type": "string" 220 | }, 221 | { 222 | "type": "object", 223 | "propertyNames": { 224 | "$ref": "#/definitions/arch" 225 | }, 226 | "patternProperties": { 227 | "": { 228 | "type": "string" 229 | } 230 | } 231 | } 232 | ] 233 | }, 234 | "url": { 235 | "description": "URL which specifies a direct download link to a single lua file. Precludes the use of `remote`, `path`.", 236 | "type": "string", 237 | "format": "uri" 238 | }, 239 | "checksum": { 240 | "description": "SHA256 digest of the file specified in `url`", 241 | "$ref": "#/definitions/checksum" 242 | }, 243 | "extra": { 244 | "description": "Dictionary which holds any desired extra information", 245 | "type": "object", 246 | "examples": [ 247 | { 248 | "author": "Ford Prefect", 249 | "license": "MIT" 250 | } 251 | ] 252 | }, 253 | "files": { 254 | "description": "Files that are downloaded and placed inside the addon directory. If the file is an archive, it will be automatically extracted.", 255 | "$ref": "#/definitions/files" 256 | } 257 | }, 258 | "required": [ 259 | "id", "version" 260 | ] 261 | } 262 | }, 263 | "lite-xls": { 264 | "description": "Represents the different versions of Lite XL that are registered in this repository", 265 | "type": "array", 266 | "items": { 267 | "type": "object", 268 | "properties": { 269 | "version": {}, 270 | "mod_version": { 271 | "description": "The modversion the binary corresponds to", 272 | "$ref": "#/definitions/mod-version" 273 | }, 274 | "files": { 275 | "description": "Files that are downloaded. If the file is an archive, it will be automatically extracted. Conventionally, there should be a single file per architecture that is a `.tar.gz` or `.zip` containing all necessary files for Lite XL to run.", 276 | "$ref": "#/definitions/files" 277 | } 278 | } 279 | } 280 | } 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "addons": [ 3 | { 4 | "id": "plugin_manager", 5 | "version": "0.1", 6 | "description": "A GUI interface to Adam's lite-xl plugin manager.", 7 | "path": "plugins/plugin_manager", 8 | "mod_version": 3, 9 | "files": [ 10 | { 11 | "url": "https://github.com/lite-xl/lite-xl-plugin-manager/releases/download/latest/lpm.x86_64-linux", 12 | "arch": "x86_64-linux", 13 | "checksum": "SKIP", 14 | "optional": true 15 | }, 16 | { 17 | "url": "https://github.com/lite-xl/lite-xl-plugin-manager/releases/download/latest/lpm.aarch64-linux", 18 | "arch": "aarch64-linux", 19 | "checksum": "SKIP", 20 | "optional": true 21 | }, 22 | { 23 | "url": "https://github.com/lite-xl/lite-xl-plugin-manager/releases/download/latest/lpm.x86_64-windows.exe", 24 | "arch": "x86_64-windows", 25 | "checksum": "SKIP", 26 | "optional": true 27 | }, 28 | { 29 | "url": "https://github.com/lite-xl/lite-xl-plugin-manager/releases/download/latest/lpm.x86_64-darwin", 30 | "arch": "x86_64-darwin", 31 | "checksum": "SKIP", 32 | "optional": true 33 | }, 34 | { 35 | "url": "https://github.com/lite-xl/lite-xl-plugin-manager/releases/download/latest/lpm.aarch64-darwin", 36 | "arch": "aarch64-darwin", 37 | "checksum": "SKIP", 38 | "optional": true 39 | }, 40 | { 41 | "url": "https://github.com/lite-xl/lite-xl-plugin-manager/releases/download/latest/lpm.aarch64-android", 42 | "arch": "aarch64-android", 43 | "checksum": "SKIP", 44 | "optional": true 45 | }, 46 | { 47 | "url": "https://github.com/lite-xl/lite-xl-plugin-manager/releases/download/latest/lpm.arm-android", 48 | "arch": "arm-android", 49 | "checksum": "SKIP", 50 | "optional": true 51 | }, 52 | { 53 | "url": "https://github.com/lite-xl/lite-xl-plugin-manager/releases/download/latest/lpm.x86-android", 54 | "arch": "x86-android", 55 | "checksum": "SKIP", 56 | "optional": true 57 | }, 58 | { 59 | "url": "https://github.com/lite-xl/lite-xl-plugin-manager/releases/download/latest/lpm.x86_64-android", 60 | "arch": "x86_64-android", 61 | "checksum": "SKIP", 62 | "optional": true 63 | } 64 | ], 65 | "dependencies": { 66 | "json": {} 67 | } 68 | }, 69 | { 70 | "id": "welcome", 71 | "description": "A welcome screen for lite-xl, that on first start, invites a user to install the meta_addons package, or access the plugin manager.", 72 | "type": "plugin", 73 | "path": "plugins/welcome.lua", 74 | "version": "0.1", 75 | "dependencies": { 76 | "plugin_manager": {} 77 | } 78 | }, 79 | { 80 | "id": "json", 81 | "version": "1.0", 82 | "description": "JSON support plugin, provides encoding/decoding.", 83 | "type": "library", 84 | "path": "libraries/json.lua" 85 | } 86 | ], 87 | "lite-xls": [ 88 | { 89 | "version": "2.1.1-simplified", 90 | "mod_version": 3, 91 | "files": [ 92 | { 93 | "arch": "x86_64-linux", 94 | "url": "https://github.com/adamharrison/lite-xl-simplified/releases/download/v2.1.1-simplified/lite-xl-v2.1.1-simplified-x86_64-linux.tar.gz", 95 | "checksum": "6a0589cf822e04563330bcf4d26b70be9a59ac268fbbd15073ae45ebb292917e" 96 | }, 97 | { 98 | "arch": "x86_64-windows", 99 | "url": "https://github.com/adamharrison/lite-xl-simplified/releases/download/v2.1.1-simplified/lite-xl-v2.1.1-simplified-x86_64-windows.zip", 100 | "checksum": "15aca182dbf3768c6f9366d3715cca2154b1a1fbce8d79794aa350f9e24ca2bf" 101 | }, 102 | { 103 | "arch": "x86_64-darwin", 104 | "url": "https://github.com/adamharrison/lite-xl-simplified/releases/download/v2.1.1-simplified/lite-xl-v2.1.1-simplified-x86_64-darwin.tar.gz", 105 | "checksum": "222f227adede48eb41fe98a800e3c67256a48ba2cc68bc8a9cce36ff9866e26c" 106 | } 107 | ] 108 | } 109 | ], 110 | "remotes": [ 111 | "https://github.com/lite-xl/lite-xl-plugins.git:master", 112 | "https://github.com/lite-xl/lite-xl-colors.git:master" 113 | ] 114 | } 115 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('lpm', 2 | ['c'], 3 | license : 'LPM', 4 | meson_version : '>= 0.56', 5 | ) 6 | 7 | cc = meson.get_compiler('c') 8 | 9 | zlib_dep = dependency('zlib') 10 | lzma_dep = dependency('liblzma') 11 | libgit2_dep = dependency('libgit2') 12 | libzip_dep = dependency('libzip') 13 | lua_dep = dependency('lua') 14 | mbedtls_dep = [ 15 | dependency('mbedtls'), 16 | dependency('mbedx509'), 17 | dependency('mbedcrypto'), 18 | ] 19 | 20 | microtar_lib = static_library('microtar', files('lib/microtar/src/microtar.c')) 21 | microtar_dep = declare_dependency( 22 | link_whole: [microtar_lib], 23 | include_directories: ['lib/microtar/src'] 24 | ) 25 | 26 | lpm_source = files('src/lpm.c') 27 | cflags = [] 28 | if get_option('static') 29 | lua_exe = find_program('lua') 30 | 31 | lpm_source += configure_file( 32 | capture: false, 33 | command: [lua_exe, '-e', 'f = string.dump(assert(load(io.lines("@INPUT0@", "L"), "=lpm.lua"))) io.open("@OUTPUT0@", "wb"):write("unsigned char lpm_luac[] = \"" .. f:gsub(".", function (c) return string.format("\\\x%02X",string.byte(c)) end) .. "\";unsigned int lpm_luac_len = " .. #f .. ";")'], 34 | input: files('src/lpm.lua'), 35 | output: 'lpm.lua.c' 36 | ) 37 | cflags += '-DLPM_STATIC' 38 | endif 39 | 40 | lpm_exe = executable('lpm', 41 | lpm_source, 42 | dependencies: [ 43 | zlib_dep, 44 | lzma_dep, 45 | mbedtls_dep, 46 | libgit2_dep, 47 | libzip_dep, 48 | lua_dep, 49 | microtar_dep 50 | ], 51 | c_args: cflags, 52 | install: true, 53 | ) 54 | 55 | if (get_option('install_plugin')) 56 | lite_datadir = get_option('lite_datadir') 57 | if lite_datadir == '' 58 | # No path given, assume a default 59 | lite_datadir = get_option('datadir') + '/lite-xl' 60 | endif 61 | 62 | install_subdir('plugins', install_dir : lite_datadir) 63 | endif 64 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('static', type : 'boolean', value : false, description: 'Build the pre-packaged lua file into the executable.') 2 | option('install_plugin', type : 'boolean', value : false, description: 'Install the Lite-XL plugin for lpm') 3 | option('lite_datadir', type : 'string', value : '', description: 'Path to the Lite-XL data directory, usually \'share/lite-xl\'') 4 | -------------------------------------------------------------------------------- /plugins/plugin_manager/init.lua: -------------------------------------------------------------------------------- 1 | -- mod-version:3 --lite-xl 2.1 --priority:5 2 | 3 | local core = require "core" 4 | local common = require "core.common" 5 | local config = require "core.config" 6 | local command = require "core.command" 7 | local json = require "libraries.json" 8 | local keymap = require "core.keymap" 9 | 10 | 11 | local PluginManager = { 12 | last_refresh = nil, 13 | requires_restart = false 14 | } 15 | local binary_extension = (PLATFORM == "Windows" and ".exe" or (PLATFORM == "Android" and ".so" or "")) 16 | config.plugins.plugin_manager = common.merge({ 17 | lpm_binary_name = "lpm." .. ARCH .. binary_extension, 18 | lpm_binary_path = nil, 19 | show_libraries = false, 20 | -- Restarts the plugin manager on changes. 21 | restart_on_change = true, 22 | -- Path to a local copy of all repositories. 23 | cachdir = nil, 24 | -- Path to the folder that holds user-specified plugins. 25 | userdir = USERDIR, 26 | -- Path to ssl certificate directory or bunde. Nil will auto-detect. 27 | ssl_certs = nil, 28 | -- Whether or not to force install things. 29 | force = false, 30 | -- Dumps commands that run to stdout, as well as responses from lpm. 31 | debug = false, 32 | -- A list of addons to apply to the system bottle. 33 | addons = nil 34 | }, config.plugins.plugin_manager) 35 | 36 | if not config.plugins.plugin_manager.lpm_binary_path then 37 | local paths = { 38 | DATADIR .. PATHSEP .. "plugins" .. PATHSEP .. "plugin_manager" .. PATHSEP .. config.plugins.plugin_manager.lpm_binary_name, 39 | USERDIR .. PATHSEP .. "plugins" .. PATHSEP .. "plugin_manager" .. PATHSEP .. config.plugins.plugin_manager.lpm_binary_name, 40 | DATADIR .. PATHSEP .. "plugins" .. PATHSEP .. "plugin_manager" .. PATHSEP .. "lpm" .. binary_extension, 41 | USERDIR .. PATHSEP .. "plugins" .. PATHSEP .. "plugin_manager" .. PATHSEP .. "lpm" .. binary_extension, 42 | } 43 | local path, s = os.getenv("PATH"), 1 44 | while true do 45 | local _, e = path:find(":", s) 46 | table.insert(paths, path:sub(s, e and (e-1) or #path) .. PATHSEP .. config.plugins.plugin_manager.lpm_binary_name) 47 | table.insert(paths, path:sub(s, e and (e-1) or #path) .. PATHSEP .. "lpm" .. binary_extension) 48 | if not e then break end 49 | s = e + 1 50 | end 51 | for i, path in ipairs(paths) do 52 | if system.get_file_info(path) then 53 | config.plugins.plugin_manager.lpm_binary_path = path 54 | break 55 | end 56 | end 57 | end 58 | if not config.plugins.plugin_manager.lpm_binary_path then error("can't find lpm binary, please supply one with config.plugins.plugin_manager.lpm_binary_path") end 59 | 60 | local Promise = { } 61 | function Promise:__index(idx) return rawget(self, idx) or Promise[idx] end 62 | function Promise.new(result) return setmetatable({ result = result, success = nil, _done = { }, _fail = { } }, Promise) end 63 | function Promise:done(done) if self.success == true then done(self.result) else table.insert(self._done, done) end return self end 64 | function Promise:fail(fail) if self.success == false then fail(self.result) else table.insert(self._fail, fail) end return self end 65 | function Promise:resolve(result) self.result = result self.success = true for i,v in ipairs(self._done) do v(result) end return self end 66 | function Promise:reject(result) self.result = result self.success = false for i,v in ipairs(self._fail) do v(result) end return self end 67 | function Promise:forward(promise) self:done(function(data) promise:resolve(data) end) self:fail(function(data) promise:reject(data) end) return self end 68 | 69 | local function join(joiner, t) local s = "" for i,v in ipairs(t) do if i > 1 then s = s .. joiner end s = s .. v end return s end 70 | 71 | local running_processes = {} 72 | local default_arguments = { 73 | "--mod-version=" .. (rawget(_G, "MOD_VERSION") or MOD_VERSION_STRING), -- #workaround hack for new system. 74 | "--datadir=" .. DATADIR, 75 | "--binary=" .. EXEFILE, 76 | "--assume-yes" 77 | } 78 | if config.plugins.plugin_manager.ssl_certs then table.insert(default_arguments, "--ssl_certs") table.insert(cmd, config.plugins.plugin_manager.ssl_certs) end 79 | if config.plugins.plugin_manager.force then table.insert(default_arguments, "--force") end 80 | 81 | local function extract_progress(chunk) 82 | local newline = chunk:find("\n") 83 | if not newline then return nil, chunk end 84 | if #chunk == newline then 85 | if chunk:find("^{\"progress\"") then return chunk, "" end 86 | return nil, chunk 87 | end 88 | return chunk:sub(1, newline - 1), chunk:sub(newline + 1) 89 | end 90 | 91 | local function run(cmd, options) 92 | options = options or {} 93 | table.insert(cmd, 1, config.plugins.plugin_manager.lpm_binary_path) 94 | table.insert(cmd, "--json") 95 | table.insert(cmd, "--quiet") 96 | table.insert(cmd, "--progress") 97 | if options.cachedir then table.insert(cmd, "--cachedir=" .. options.cachedir) end 98 | table.insert(cmd, "--userdir=" .. (options.userdir or USERDIR)) 99 | for i,v in ipairs(default_arguments) do table.insert(cmd, v) end 100 | local proc = process.start(cmd) 101 | if config.plugins.plugin_manager.debug then for i, v in ipairs(cmd) do io.stdout:write((i > 1 and " " or "") .. v) end io.stdout:write("\n") io.stdout:flush() end 102 | local promise = Promise.new() 103 | table.insert(running_processes, { proc, promise, "" }) 104 | if #running_processes == 1 then 105 | core.add_thread(function() 106 | while #running_processes > 0 do 107 | local still_running_processes = {} 108 | local has_chunk = false 109 | local i = 1 110 | while i < #running_processes + 1 do 111 | local v = running_processes[i] 112 | local still_running = true 113 | local progress_line 114 | while true do 115 | local chunk = v[1]:read_stdout(2048) 116 | if config.plugins.plugin_manager.debug and chunk ~= nil then io.stdout:write(chunk) io.stdout:flush() end 117 | if chunk and #chunk == 0 then break end 118 | if chunk ~= nil and #chunk > 0 then 119 | v[3] = v[3] .. chunk 120 | progress_line, v[3] = extract_progress(v[3]) 121 | if options.progress and progress_line then 122 | progress_line = json.decode(progress_line) 123 | options.progress(progress_line.progress) 124 | end 125 | has_chunk = true 126 | else 127 | still_running = false 128 | if v[1]:returncode() == 0 then 129 | progress_line, v[3] = extract_progress(v[3]) 130 | v[2]:resolve(v[3]) 131 | else 132 | local err = v[1]:read_stderr(2048) 133 | core.error("error running " .. join(" ", cmd) .. ": " .. (err or "?")) 134 | progress_line, v[3] = extract_progress(v[3]) 135 | if err then 136 | v[2]:reject(json.decode(err).error) 137 | else 138 | v[2]:reject(err) 139 | end 140 | end 141 | break 142 | end 143 | end 144 | if still_running then 145 | table.insert(still_running_processes, v) 146 | end 147 | i = i + 1 148 | end 149 | running_processes = still_running_processes 150 | coroutine.yield(has_chunk and 0.001 or 0.05) 151 | end 152 | end) 153 | end 154 | return promise 155 | end 156 | 157 | 158 | function PluginManager:refresh(options) 159 | local prom = Promise.new() 160 | local cmd = { "list" } 161 | if not config.plugins.plugin_manager.show_libraries then 162 | table.insert(cmd, "--type") 163 | table.insert(cmd, "!library") 164 | end 165 | run(cmd, options):done(function(addons) 166 | self.addons = json.decode(addons)["addons"] 167 | table.sort(self.addons, function(a,b) return a.id < b.id end) 168 | self.valid_addons = {} 169 | for i, addon in ipairs(self.addons) do 170 | if addon.status ~= "incompatible" then 171 | table.insert(self.valid_addons, addon) 172 | if (addon.id == "plugin_manager" or addon.id == "json") and (addon.status == "installed" or addon.status == "orphan") then 173 | addon.status = "special" 174 | end 175 | end 176 | end 177 | self.last_refresh = os.time() 178 | core.redraw = true 179 | prom:resolve(addons) 180 | run({ "repo", "list" }, options):done(function(repositories) 181 | self.repositories = json.decode(repositories)["repositories"] 182 | end) 183 | end) 184 | return prom 185 | end 186 | 187 | 188 | function PluginManager:upgrade(options) 189 | local prom = Promise.new() 190 | run({ "update" }, options):done(function() 191 | run({ "upgrade" }, options):done(function() 192 | prom:resolve() 193 | end) 194 | end) 195 | return prom 196 | end 197 | 198 | 199 | 200 | function PluginManager:purge(options) 201 | return run({ "purge" }, options) 202 | end 203 | 204 | 205 | function PluginManager:get_addons(options) 206 | local prom = Promise.new() 207 | if self.addons then 208 | prom:resolve(self.addons) 209 | else 210 | self:refresh(options):done(function() 211 | prom:resolve(self.addons) 212 | end):fail(function(arg) promise:reject(arg) end) 213 | end 214 | return prom 215 | end 216 | 217 | local function run_stateful_plugin_command(plugin_manager, cmd, args, options) 218 | local promise = Promise.new() 219 | run({ cmd, table.unpack(args) }, options):done(function(result) 220 | if (options.restart == nil and config.plugins.plugin_manager.restart_on_change) or options.restart then 221 | command.perform("core:restart") 222 | else 223 | plugin_manager:refresh(options):forward(promise) 224 | end 225 | end):fail(function(arg) 226 | promise:reject(arg) 227 | end) 228 | return promise 229 | end 230 | 231 | 232 | function PluginManager:install(addon, options) return run_stateful_plugin_command(self, "install", { addon.id .. (addon.version and (":" .. addon.version) or "") }, options) end 233 | function PluginManager:reinstall(addon, options) return run_stateful_plugin_command(self, "install", { addon.id .. (addon.version and (":" .. addon.version) or ""), "--reinstall" }, options) end 234 | function PluginManager:uninstall(addon, options) return run_stateful_plugin_command(self, "uninstall", { addon.id }, options) end 235 | function PluginManager:unstub(addon, options) 236 | local promise = Promise.new() 237 | if addon.path and system.get_file_info(addon.path) then 238 | promise:resolve(addon) 239 | else 240 | run({ "unstub", addon.id }, options):done(function(result) 241 | local unstubbed_addon = json.decode(result).addons[1] 242 | for k,v in pairs(unstubbed_addon) do addon[k] = v end 243 | promise:resolve(addon) 244 | end):fail(function(arg) promise:reject(arg) end) 245 | end 246 | return promise 247 | end 248 | 249 | 250 | function PluginManager:get_addon(name_and_version, options) 251 | local promise = Promise.new() 252 | PluginManager:get_addons(options):done(function() 253 | local s = name_and_version:find(":") 254 | local name, version = name_and_version, nil 255 | if s then 256 | name = name_and_version:sub(1, s-1) 257 | version = name_and_version:sub(s+1) 258 | end 259 | local match = false 260 | for i, addon in ipairs(PluginManager.addons) do 261 | if not addon.mod_version or tostring(addon.mod_version) == tostring(rawget(_G, "MOD_VERSION_MAJOR") or rawget(_G, "MOD_VERSION")) and (addon.version == version or version == nil) then 262 | promise:resolve(addon) 263 | match = true 264 | break 265 | end 266 | end 267 | if not match then promise:reject() end 268 | end):fail(function(arg) promise:reject(arg) end) 269 | return promise 270 | end 271 | 272 | PluginManager.promise = Promise 273 | PluginManager.view = require "plugins.plugin_manager.plugin_view" 274 | 275 | -- This will be significantly simplified when the plugin loading monolith is broken up a bit. 276 | if config.plugins.plugin_manager.addons then 277 | local target_plugin_directory = config.plugins.plugin_manger.adddon_directory or (USERDIR .. PATHSEP .. "projects" .. PATHSEP .. common.basename(system.absolute_path("."))) 278 | 279 | local mod_version_regex = 280 | regex.compile([[--.*mod-version:(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:$|\s)]]) 281 | local function get_plugin_details(filename) 282 | local info = system.get_file_info(filename) 283 | if info ~= nil and info.type == "dir" then 284 | filename = filename .. PATHSEP .. "init.lua" 285 | info = system.get_file_info(filename) 286 | end 287 | if not info or not filename:match("%.lua$") then return false end 288 | local f = io.open(filename, "r") 289 | if not f then return false end 290 | local priority = false 291 | local version_match = false 292 | local major, minor, patch 293 | 294 | for line in f:lines() do 295 | if not version_match then 296 | local _major, _minor, _patch = mod_version_regex:match(line) 297 | if _major then 298 | _major = tonumber(_major) or 0 299 | _minor = tonumber(_minor) or 0 300 | _patch = tonumber(_patch) or 0 301 | major, minor, patch = _major, _minor, _patch 302 | 303 | version_match = major == MOD_VERSION_MAJOR 304 | if version_match then 305 | version_match = minor <= MOD_VERSION_MINOR 306 | end 307 | if version_match then 308 | version_match = patch <= MOD_VERSION_PATCH 309 | end 310 | end 311 | end 312 | 313 | if not priority then 314 | priority = line:match('%-%-.*%f[%a]priority%s*:%s*(%d+)') 315 | if priority then priority = tonumber(priority) end 316 | end 317 | 318 | if version_match then 319 | break 320 | end 321 | end 322 | f:close() 323 | return true, { 324 | version_match = version_match, 325 | version = major and {major, minor, patch} or {}, 326 | priority = priority or 100 327 | } 328 | end 329 | 330 | local function replace_string(str, text, replace) 331 | local offset = 1 332 | local result = "" 333 | while true do 334 | local s,e = str:find(text, offset, true) 335 | if s then 336 | result = result .. str:sub(offset, s - 1) .. replace 337 | offset = e + 1 338 | else 339 | result = result .. str:sub(offset) 340 | break 341 | end 342 | end 343 | return result 344 | end 345 | 346 | local function lpm_load_plugins() 347 | package.cpath = replace_string(package.cpath, USERDIR, target_plugin_directory) 348 | package.path = replace_string(package.path, USERDIR, target_plugin_directory) 349 | 350 | local no_errors = true 351 | local refused_list = { 352 | userdir = {dir = target_plugin_directory, plugins = {}} 353 | } 354 | local files, ordered = {}, {} 355 | for _, root_dir in ipairs {target_plugin_directory} do 356 | local plugin_dir = root_dir .. PATHSEP .. "plugins" 357 | for _, filename in ipairs(system.list_dir(plugin_dir) or {}) do 358 | if not files[filename] then 359 | table.insert( 360 | ordered, {file = filename} 361 | ) 362 | end 363 | -- user plugins will always replace system plugins 364 | files[filename] = plugin_dir 365 | end 366 | end 367 | 368 | for _, plugin in ipairs(ordered) do 369 | local dir = files[plugin.file] 370 | local name = plugin.file:match("(.-)%.lua$") or plugin.file 371 | local is_lua_file, details = get_plugin_details(dir .. PATHSEP .. plugin.file) 372 | 373 | plugin.valid = is_lua_file 374 | plugin.name = name 375 | plugin.dir = dir 376 | plugin.priority = details and details.priority or 100 377 | plugin.version_match = details and details.version_match or false 378 | plugin.version = details and details.version or {} 379 | plugin.version_string = #plugin.version > 0 and table.concat(plugin.version, ".") or "unknown" 380 | end 381 | 382 | -- sort by priority or name for plugins that have same priority 383 | table.sort(ordered, function(a, b) 384 | if a.priority ~= b.priority then 385 | return a.priority < b.priority 386 | end 387 | return a.name < b.name 388 | end) 389 | 390 | local load_start = system.get_time() 391 | for _, plugin in ipairs(ordered) do 392 | if plugin.valid then 393 | if not config.skip_plugins_version and not plugin.version_match then 394 | core.log_quiet( 395 | "Version mismatch for plugin %q[%s] from %s", 396 | plugin.name, 397 | plugin.version_string, 398 | plugin.dir 399 | ) 400 | local rlist = plugin.dir:find(USERDIR, 1, true) == 1 401 | and 'userdir' or 'datadir' 402 | local list = refused_list[rlist].plugins 403 | table.insert(list, plugin) 404 | elseif config.plugins[plugin.name] ~= false then 405 | local start = system.get_time() 406 | local ok, loaded_plugin = core.try(require, "plugins." .. plugin.name) 407 | if ok then 408 | local plugin_version = "" 409 | if plugin.version_string ~= MOD_VERSION_STRING then 410 | plugin_version = "["..plugin.version_string.."]" 411 | end 412 | core.log_quiet( 413 | "Loaded plugin %q%s from %s in %.1fms", 414 | plugin.name, 415 | plugin_version, 416 | plugin.dir, 417 | (system.get_time() - start) * 1000 418 | ) 419 | end 420 | if not ok then 421 | no_errors = false 422 | elseif config.plugins[plugin.name].onload then 423 | core.try(config.plugins[plugin.name].onload, loaded_plugin) 424 | end 425 | end 426 | end 427 | end 428 | core.log_quiet( 429 | "Loaded all managed plugins in %.1fms", 430 | (system.get_time() - load_start) * 1000 431 | ) 432 | return no_errors, refused_list 433 | end 434 | 435 | local addons = {} 436 | local added_addons = {} 437 | for i,v in ipairs(config.plugins.plugin_manager.addons) do 438 | if type(v) == 'table' then 439 | local string = "" 440 | if v.remote then 441 | string = v.remote 442 | if v.commit or v.branch then 443 | string = string .. ":" .. (v.commit or v.branch) 444 | end 445 | string = string .. "@" 446 | end 447 | if not v.id then error("requires config.plugin_manager.addons entries to have an id") end 448 | string = string .. v.id 449 | if v.version then string = string .. ":" .. v.version end 450 | table.insert(addons, string) 451 | added_addons[v.id] = true 452 | else 453 | table.insert(addons, v) 454 | added_addons[v] = true 455 | end 456 | end 457 | local plugins = system.list_dir(USERDIR .. PATHSEP .. "plugins") 458 | run({ "apply", table.unpack(addons), }, { userdir = target_plugin_directory }):done(function(status) 459 | if json.decode(status)["changed"] then command.perform("core:restart") end 460 | end) 461 | lpm_load_plugins() 462 | local old_configs = {} 463 | for i,v in ipairs(plugins or {}) do 464 | local id = v:gsub("%.lua$", "") 465 | if config.plugins[id] ~= false and id ~= "plugin_manager" and not added_addons[id] then 466 | old_configs[id] = config.plugins[id] 467 | config.plugins[id] = false 468 | end 469 | end 470 | end 471 | 472 | 473 | command.add(nil, { 474 | ["plugin-manager:install"] = function() 475 | PluginManager:get_addons({ progress = PluginManager.view.progress_callback }) 476 | core.command_view:enter("Enter plugin name", 477 | function(name) 478 | PluginManager:get_addon(name, { progress = PluginManager.view.progress_callback }):done(function(addon) 479 | core.log("Attempting to install plugin " .. name .. "...") 480 | PluginManager:install(addon, { progress = PluginManager.view.progress_callback }):done(function() 481 | core.log("Successfully installed plugin " .. addon.id .. ".") 482 | end) 483 | end):fail(function() 484 | core.error("Unknown plugin " .. name .. ".") 485 | end) 486 | end, 487 | function(text) 488 | local items = {} 489 | if not PluginManager.addons then return end 490 | for i, addon in ipairs(PluginManager.addons) do 491 | if not addon.mod_version or tostring(addon.mod_version) == tostring(MOD_VERSION) and addon.status == "available" then 492 | table.insert(items, addon.id .. ":" .. addon.version) 493 | end 494 | end 495 | return common.fuzzy_match(items, text) 496 | end 497 | ) 498 | end, 499 | ["plugin-manager:uninstall"] = function() 500 | PluginManager:get_addons({ progress = PluginManager.view.progress_callback }) 501 | core.command_view:enter("Enter plugin name", 502 | function(name) 503 | PluginManager:get_addon(name, { progress = PluginManager.view.progress_callback }):done(function(addon) 504 | core.log("Attempting to uninstall plugin " .. addon.id .. "...") 505 | PluginManager:uninstall(addon, { progress = PluginManager.view.progress_callback }):done(function() 506 | core.log("Successfully uninstalled plugin " .. addon.id .. ".") 507 | end) 508 | end):fail(function() 509 | core.error("Unknown plugin " .. name .. ".") 510 | end) 511 | end, 512 | function(text) 513 | local items = {} 514 | if not PluginManager.addons then return end 515 | for i, addon in ipairs(PluginManager.addons) do 516 | if addon.status == "installed" then 517 | table.insert(items, addon.id .. ":" .. addon.version) 518 | end 519 | end 520 | return common.fuzzy_match(items, text) 521 | end 522 | ) 523 | end, 524 | ["plugin-manager:add-repository"] = function() 525 | core.command_view:enter("Enter repository url", 526 | function(url) 527 | PluginManager:add(url):done(function() 528 | core.log("Successfully added repository " .. url .. ".") 529 | end) 530 | end 531 | ) 532 | end, 533 | ["plugin-manager:remove-repository"] = function() 534 | PluginManager:get_addons({ progress = PluginManager.view.progress_callback }) 535 | core.command_view:enter("Enter repository url", 536 | function(url) 537 | PluginManager:remove(url):done(function() 538 | core.log("Successfully removed repository " .. url .. ".") 539 | end) 540 | end, 541 | function(text) 542 | local items = {} 543 | if PluginManager.repositories then 544 | for i,v in ipairs(PluginManager.repositories) do 545 | table.insert(items, v.remote .. ":" .. (v.commit or v.branch)) 546 | end 547 | end 548 | return common.fuzzy_match(items, text) 549 | end 550 | ) 551 | end, 552 | ["plugin-manager:refresh"] = function() PluginManager:refresh({ progress = PluginManager.view.progress_callback }):done(function() core.log("Successfully refreshed plugin listing.") end) end, 553 | ["plugin-manager:upgrade"] = function() PluginManager:upgrade({ progress = PluginManager.view.progress_callback }):done(function() core.log("Successfully upgraded installed plugins.") end) end, 554 | ["plugin-manager:purge"] = function() PluginManager:purge({ progress = PluginManager.view.progress_callback }):done(function() core.log("Successfully purged lpm directory.") end) end, 555 | ["plugin-manager:show"] = function() 556 | local node = core.root_view:get_active_node_default() 557 | node:add_view(PluginManager.view(PluginManager)) 558 | end, 559 | }) 560 | 561 | if pcall(require, "plugins.terminal") then 562 | local terminal = require "plugins.terminal" 563 | command.add(nil, { 564 | ["plugin-manager:open-session"] = function() 565 | local arguments = { "-", "--userdir=" .. USERDIR } 566 | for i,v in ipairs(default_arguments) do table.insert(arguments, v) end 567 | local tv = terminal.class(common.merge(config.plugins.terminal, { 568 | shell = config.plugins.plugin_manager.lpm_binary_path, 569 | arguments = arguments 570 | })) 571 | core.root_view:get_active_node_default():add_view(tv) 572 | end 573 | }) 574 | end 575 | 576 | return PluginManager 577 | -------------------------------------------------------------------------------- /plugins/plugin_manager/plugin_view.lua: -------------------------------------------------------------------------------- 1 | 2 | local core = require "core" 3 | local style = require "core.style" 4 | local common = require "core.common" 5 | local config = require "core.config" 6 | local command = require "core.command" 7 | local json = require "libraries.json" 8 | local View = require "core.view" 9 | local keymap = require "core.keymap" 10 | local RootView = require "core.rootview" 11 | local ContextMenu = require "core.contextmenu" 12 | 13 | local PluginView = View:extend() 14 | 15 | 16 | local function join(joiner, t) 17 | local s = "" 18 | for i,v in ipairs(t) do if i > 1 then s = s .. joiner end s = s .. v end 19 | return s 20 | end 21 | 22 | 23 | local plugin_view = nil 24 | PluginView.menu = ContextMenu() 25 | 26 | function PluginView:new() 27 | PluginView.super.new(self) 28 | self.scrollable = true 29 | self.progress = nil 30 | self.show_incompatible_plugins = false 31 | self.plugin_table_columns = { "Name", "Version", "Type", "Status", "Tags", "Author", "Description" } 32 | self.hovered_plugin = nil 33 | self.hovered_plugin_idx = nil 34 | self.selected_plugin = nil 35 | self.selected_plugin_idx = nil 36 | self.initialized = false 37 | self.plugin_manager = require "plugins.plugin_manager" 38 | self.sort = { asc = true, column = 1 } 39 | self.progress_callback = function(progress) 40 | self.progress = progress 41 | core.redraw = true 42 | end 43 | self:refresh() 44 | plugin_view = self 45 | end 46 | 47 | local function get_plugin_text(plugin) 48 | return (plugin.name or plugin.id), (plugin.status == "core" and VERSION or plugin.version), plugin.type, plugin.status, join(", ", plugin.tags), plugin.author or "unknown", plugin.description-- (plugin.description or ""):gsub("%[[^]+%]%([^)]+%)", "") 49 | end 50 | 51 | 52 | function PluginView:get_name() 53 | return "Plugin Manager" 54 | end 55 | 56 | 57 | local root_view_update = RootView.update 58 | function RootView:update(...) 59 | root_view_update(self, ...) 60 | PluginView.menu:update() 61 | end 62 | 63 | 64 | local root_view_draw = RootView.draw 65 | function RootView:draw(...) 66 | root_view_draw(self, ...) 67 | PluginView.menu:draw() 68 | end 69 | 70 | 71 | local root_view_on_mouse_moved = RootView.on_mouse_moved 72 | function RootView:on_mouse_moved(...) 73 | if PluginView.menu:on_mouse_moved(...) then return end 74 | return root_view_on_mouse_moved(self, ...) 75 | end 76 | 77 | 78 | local on_view_mouse_pressed = RootView.on_view_mouse_pressed 79 | function RootView.on_view_mouse_pressed(button, x, y, clicks) 80 | local handled = PluginView.menu:on_mouse_pressed(button, x, y, clicks) 81 | return handled or on_view_mouse_pressed(button, x, y, clicks) 82 | end 83 | 84 | 85 | function PluginView:on_mouse_pressed(button, mx, my, clicks) 86 | if PluginView.super.on_mouse_pressed(self, button, mx, my, clicks) then return true end 87 | local lh = style.font:get_height() + style.padding.y 88 | local x = self:get_content_offset() + style.padding.x 89 | 90 | local column_headers_y = self.position.y + (self.filter_text and lh or 0) 91 | 92 | if my > column_headers_y and my <= column_headers_y + lh then 93 | for i, _ in ipairs(self.plugin_table_columns) do 94 | if mx > x and mx <= x + self.widths[i] then 95 | if i == self.sort.column then self.sort.asc = not self.sort.asc else self.sort.asc = true end 96 | self.sort.column = i 97 | self:refresh(true) 98 | return true 99 | end 100 | x = x + self.widths[i] + style.padding.x 101 | end 102 | end 103 | return false 104 | end 105 | 106 | 107 | function PluginView:on_mouse_moved(x, y, dx, dy) 108 | PluginView.super.on_mouse_moved(self, x, y, dx, dy) 109 | if self.initialized then 110 | local th = style.font:get_height() 111 | local lh = th + style.padding.y 112 | local offset = math.floor((y - self.position.y - (self.filter_text and self.filter_text ~= "" and lh or 0) + self.scroll.y) / lh) 113 | self.hovered_plugin = offset > 0 and self:get_sorted_plugins()[offset] 114 | self.hovered_plugin_idx = offset > 0 and offset 115 | end 116 | end 117 | 118 | 119 | function PluginView:refresh(no_refetch, on_complete) 120 | if self.loading then return end 121 | self.loading = true 122 | local function complete() 123 | self.loading = false 124 | self.initialized = true 125 | self.widths = {} 126 | self.sorted_plugins = {} 127 | for i,v in ipairs(self.plugin_table_columns) do 128 | table.insert(self.widths, style.font:get_width(v)) 129 | end 130 | for i, plugin in ipairs(self:get_plugins()) do 131 | local t = { get_plugin_text(plugin) } 132 | for j = 1, #self.widths do 133 | self.widths[j] = math.max(style.font:get_width(t[j] or ""), self.widths[j]) 134 | end 135 | if not self.filter_text or self.filter_text == "" or string.ulower(t[1]):ufind(self.filter_text, 1, true) then 136 | table.insert(self.sorted_plugins, plugin) 137 | self.sorted_plugins[plugin] = t 138 | end 139 | end 140 | table.sort(self.sorted_plugins, function(a, b) 141 | local va, vb = string.ulower(self.sorted_plugins[a][self.sort.column] or ""), string.ulower(self.sorted_plugins[b][self.sort.column] or "") 142 | if self.sort.asc then return va < vb else return va > vb end 143 | end) 144 | local max = 0 145 | if self.widths then 146 | for i, v in ipairs(self.widths) do max = max + v end 147 | end 148 | self.max_width = max + style.padding.x * #self.widths 149 | self.selected_plugin, self.selected_plugin_idx, self.hovered_plugin, self.hovered_plugin_idx = nil, nil, nil, nil 150 | core.redraw = true 151 | if on_complete then on_complete() end 152 | end 153 | return no_refetch and complete() or self.plugin_manager:refresh({ progress = self.progress_callback }):done(complete) 154 | end 155 | 156 | 157 | function PluginView:get_plugins() 158 | return self.show_incompatible_plugins and self.plugin_manager.addons or self.plugin_manager.valid_addons 159 | end 160 | 161 | 162 | function PluginView:get_sorted_plugins() 163 | return self.sorted_plugins or {} 164 | end 165 | 166 | 167 | function PluginView:get_table_content_offset() 168 | return ((self.filter_text and self.filter_text ~= "") and 2 or 1) * (style.font:get_height() + style.padding.y) 169 | end 170 | 171 | 172 | function PluginView:get_scrollable_size() 173 | if not self.initialized then return math.huge end 174 | local th = style.font:get_height() + style.padding.y 175 | return (th * #self:get_sorted_plugins()) + self:get_table_content_offset() 176 | end 177 | 178 | 179 | local function mul(color1, color2) 180 | return { color1[1] * color2[1] / 255, color1[2] * color2[2] / 255, color1[3] * color2[3] / 255, color1[4] * color2[4] / 255 } 181 | end 182 | 183 | function PluginView:get_h_scrollable_size() 184 | return self.max_width or 0 185 | end 186 | 187 | local function draw_loading_bar(x, y, width, height, percent) 188 | renderer.draw_rect(x, y, width, height, style.line_highlight) 189 | renderer.draw_rect(x, y, width * percent, height, style.caret) 190 | end 191 | 192 | function PluginView:draw_loading_screen(label, percent) 193 | common.draw_text(style.big_font, style.dim, "Loading...", "center", self.position.x, self.position.y, self.size.x, self.size.y) 194 | local width = self.size.x / 2 195 | local offset_y = self.size.y / 2 196 | if label or percent then 197 | local th = style.font:get_height() 198 | local lh = th + style.padding.y 199 | common.draw_text(style.font, style.dim, label, "center", self.position.x, self.position.y + offset_y + lh, self.size.x, lh) 200 | draw_loading_bar(self.position.x + (self.size.x / 2) - (width / 2), self.position.y + self.size.y / 2 + (lh * 2), width, lh, percent) 201 | end 202 | end 203 | 204 | function PluginView:draw() 205 | self:draw_background(style.background) 206 | local th = style.font:get_height() 207 | local lh = th + style.padding.y 208 | 209 | if not self.initialized or not self.widths then 210 | return self:draw_loading_screen(self.progress and self.progress.label, self.progress and self.progress.percent) 211 | end 212 | 213 | local header_x, header_y = self.position.x + style.padding.x, self.position.y 214 | local sorted_plugins = self:get_sorted_plugins() 215 | if self.filter_text and self.filter_text ~= "" then 216 | local total = #self:get_plugins() 217 | local msg = string.format('Showing %d out of %d plugin%s matching the criteria: %q', #sorted_plugins, total, total > 1 and "s" or "", self.filter_text) 218 | common.draw_text(style.font, style.text, msg, "left", header_x, header_y, self.size.x, lh) 219 | header_y = header_y + lh 220 | end 221 | 222 | for i, v in ipairs(self.plugin_table_columns) do 223 | if i == self.sort.column then 224 | renderer.draw_rect(header_x, header_y + (self.sort.asc and lh - 1 or 0), self.widths[i], 1, style.caret) 225 | end 226 | common.draw_text(style.font, style.accent, v, "left", header_x, header_y, self.widths[i], lh) 227 | header_x = header_x + self.widths[i] + style.padding.x 228 | end 229 | 230 | local ox, oy = self:get_content_offset() 231 | oy = oy + self:get_table_content_offset() 232 | core.push_clip_rect(self.position.x, self.position.y + self:get_table_content_offset(), self.size.x, self.size.y) 233 | for i, plugin in ipairs(sorted_plugins) do 234 | local x, y = ox, oy 235 | if y + lh >= self.position.y and y <= self.position.y + self.size.y then 236 | if plugin == self.selected_plugin then 237 | renderer.draw_rect(x, y, self.max_width or self.size.x, lh, style.dim) 238 | elseif plugin == self.hovered_plugin then 239 | renderer.draw_rect(x, y, self.max_width or self.size.x, lh, style.line_highlight) 240 | end 241 | x = x + style.padding.x 242 | for j, v in ipairs(sorted_plugins[plugin]) do 243 | local color = (plugin.status == "installed" or plugin.status == "bundled" or plugin.status == "orphan") and style.good or 244 | (plugin.status == "core" and style.warn or 245 | (plugin.status == "special" and style.modified or style.text) 246 | ) 247 | if self.loading then color = mul(color, style.dim) end 248 | common.draw_text(style.font, color, v, "left", x, y, self.widths[j], lh) 249 | x = x + self.widths[j] + style.padding.x 250 | end 251 | end 252 | oy = oy + lh 253 | end 254 | 255 | if self.loading and self.progress then 256 | draw_loading_bar(self.position.x, self.position.y, self.size.x, 2, self.progress.percent) 257 | end 258 | 259 | core.pop_clip_rect() 260 | PluginView.super.draw_scrollbar(self) 261 | end 262 | 263 | function PluginView:install(plugin) 264 | self.loading = true 265 | return self.plugin_manager:install(plugin, { progress = self.progress_callback }):done(function() 266 | self.loading = false 267 | self.selected_plugin, plugin_view.selected_plugin_idx = nil, nil 268 | end) 269 | end 270 | 271 | function PluginView:uninstall(plugin) 272 | self.loading = true 273 | return self.plugin_manager:uninstall(plugin, { progress = self.progress_callback }):done(function() 274 | self.loading = false 275 | self.selected_plugin, plugin_view.selected_plugin_idx = nil, nil 276 | end) 277 | end 278 | 279 | 280 | function PluginView:unstub(plugin) 281 | self.loading = true 282 | return self.plugin_manager:unstub(plugin, { progress = self.progress_callback }):done(function() 283 | self.loading = false 284 | end) 285 | end 286 | 287 | function PluginView:reinstall(plugin) 288 | self.loading = true 289 | return self.plugin_manager:reinstall(plugin, { progress = self.progress_callback }):done(function() 290 | self.loading = false 291 | self.selected_plugin, plugin_view.selected_plugin_idx = nil, nil 292 | end) 293 | end 294 | 295 | 296 | function PluginView:upgrade() 297 | self.loading = true 298 | return self.plugin_manager:upgrade({ progress = self.progress_callback }):done(function() 299 | self.loading = false 300 | self.selected_plugin, plugin_view.selected_plugin_idx = nil, nil 301 | end) 302 | end 303 | 304 | local function scroll_to_index(idx) 305 | local lh = style.font:get_height() + style.padding.y 306 | plugin_view.scroll.to.y = math.max(idx * lh - plugin_view.size.y / 2, 0) 307 | end 308 | 309 | command.add(PluginView, { 310 | ["plugin-manager:select"] = function(x, y) 311 | plugin_view.selected_plugin, plugin_view.selected_plugin_idx = plugin_view.hovered_plugin, plugin_view.hovered_plugin_idx 312 | end, 313 | ["plugin-manager:select-prev"] = function() 314 | local plugins = plugin_view:get_sorted_plugins() 315 | plugin_view.selected_plugin_idx = common.clamp((plugin_view.selected_plugin_idx or 0) - 1, 1, #plugins) 316 | plugin_view.selected_plugin = plugins[plugin_view.selected_plugin_idx] 317 | scroll_to_index(plugin_view.selected_plugin_idx) 318 | end, 319 | ["plugin-manager:select-next"] = function() 320 | local plugins = plugin_view:get_sorted_plugins() 321 | plugin_view.selected_plugin_idx = common.clamp((plugin_view.selected_plugin_idx or 0) + 1, 1, #plugins) 322 | plugin_view.selected_plugin = plugins[plugin_view.selected_plugin_idx] 323 | scroll_to_index(plugin_view.selected_plugin_idx) 324 | end, 325 | ["plugin-manager:find"] = function() 326 | local plugin_names = {} 327 | local plugins = plugin_view:get_plugins() 328 | for i,v in ipairs(plugins) do 329 | table.insert(plugin_names, v.id) 330 | end 331 | table.sort(plugin_names) 332 | core.command_view:enter("Find Plugin", { 333 | submit = function(value) 334 | plugin_view.filter_text = nil 335 | plugin_view:refresh(true, function() 336 | local plugins = plugin_view:get_sorted_plugins() 337 | for i,v in ipairs(plugins) do 338 | if v.id == value then 339 | plugin_view.selected_plugin_idx = i 340 | plugin_view.selected_plugin = v 341 | return scroll_to_index(plugin_view.selected_plugin_idx) 342 | end 343 | end 344 | end) 345 | end, 346 | suggest = function(value) 347 | return common.fuzzy_match(plugin_names, value) 348 | end 349 | }) 350 | end, 351 | ["plugin-manager:filter"] = function() 352 | local function on_filter(value) 353 | plugin_view.filter_text = value 354 | plugin_view:refresh(true) 355 | return {} 356 | end 357 | core.command_view:enter("Filter Plugins", { 358 | submit = on_filter, 359 | suggest = on_filter, 360 | text = plugin_view.filter_text or "", 361 | select_text = true 362 | }) 363 | end, 364 | ["plugin-manager:clear-filter"] = function() 365 | plugin_view.filter_text = nil 366 | plugin_view:refresh(true) 367 | end, 368 | ["plugin-manager:scroll-page-up"] = function() 369 | plugin_view.scroll.to.y = math.max(plugin_view.scroll.y - plugin_view.size.y, 0) 370 | end, 371 | ["plugin-manager:scroll-page-down"] = function() 372 | plugin_view.scroll.to.y = math.min(plugin_view.scroll.y + plugin_view.size.y, plugin_view:get_scrollable_size()) 373 | end, 374 | ["plugin-manager:scroll-page-top"] = function() 375 | plugin_view.scroll.to.y = 0 376 | end, 377 | ["plugin-manager:scroll-page-bottom"] = function() 378 | plugin_view.scroll.to.y = plugin_view:get_scrollable_size() 379 | end, 380 | ["plugin-manager:refresh-all"] = function() -- Separate command from `refresh`, because we want to only have the keycombo be valid on the plugin view screen. 381 | plugin_view:refresh():done(function() core.log("Successfully refreshed plugin listing.") end) 382 | end, 383 | ["plugin-manager:upgrade-all"] = function() 384 | plugin_view:upgrade():done(function() core.log("Successfully upgraded installed plugins.") end) 385 | end 386 | }) 387 | command.add(function() 388 | return core.active_view and core.active_view:is(PluginView) and plugin_view.selected_plugin and plugin_view.selected_plugin.status == "available" 389 | end, { 390 | ["plugin-manager:install-selected"] = function() plugin_view:install(plugin_view.selected_plugin) end 391 | }) 392 | command.add(function() 393 | return core.active_view and core.active_view:is(PluginView) and plugin_view.hovered_plugin and plugin_view.hovered_plugin.status == "available" 394 | end, { 395 | ["plugin-manager:install-hovered"] = function() plugin_view:install(plugin_view.hovered_plugin) end 396 | }) 397 | command.add(function() 398 | return core.active_view and core.active_view:is(PluginView) and plugin_view.selected_plugin and (plugin_view.selected_plugin.status == "installed" or plugin_view.selected_plugin.status == "orphan" or plugin_view.selected_plugin.status == "bundled") 399 | end, { 400 | ["plugin-manager:uninstall-selected"] = function() plugin_view:uninstall(plugin_view.selected_plugin) end 401 | }) 402 | command.add(function() 403 | return core.active_view and core.active_view:is(PluginView) and plugin_view.hovered_plugin and (plugin_view.hovered_plugin.status == "installed" or plugin_view.hovered_plugin.status == "orphan" or plugin_view.hovered_plugin.status == "bundled") 404 | end, { 405 | ["plugin-manager:uninstall-hovered"] = function() plugin_view:uninstall(plugin_view.hovered_plugin) end, 406 | ["plugin-manager:reinstall-hovered"] = function() plugin_view:reinstall(plugin_view.hovered_plugin) end 407 | }) 408 | command.add(function() 409 | return core.active_view and core.active_view:is(PluginView) and plugin_view.hovered_plugin 410 | end, { 411 | ["plugin-manager:view-source-hovered"] = function() 412 | plugin_view:unstub(plugin_view.hovered_plugin):done(function(plugin) 413 | local opened = false 414 | for i, path in ipairs({ plugin.path, plugin.path .. PATHSEP .. "init.lua" }) do 415 | local stat = system.get_file_info(path) 416 | if stat and stat.type == "file" then 417 | core.root_view:open_doc(core.open_doc(path)) 418 | opened = true 419 | end 420 | end 421 | if not opened then core.error("Can't find source for plugin.") end 422 | end) 423 | end, 424 | ["plugin-manager:view-readme-hovered"] = function() 425 | plugin_view:unstub(plugin_view.hovered_plugin):done(function(plugin) 426 | local opened = false 427 | local directories = { plugin.path } 428 | if plugin.repo_path then 429 | table.insert(directories, plugin.repo_path) 430 | table.insert(directories, ("" .. plugin.repo_path:gsub(PATHSEP .. "plugins" .. PATHSEP .. plugin.id .. "$", ""))) 431 | end 432 | for _, directory in ipairs(directories) do 433 | for i, path in ipairs({ directory .. PATHSEP .. "README.md", directory .. PATHSEP .. "readme.md" }) do 434 | local stat = system.get_file_info(path) 435 | if stat and stat.type == "file" then 436 | core.root_view:open_doc(core.open_doc(path)) 437 | opened = true 438 | end 439 | end 440 | end 441 | if not opened then core.error("Can't find README for plugin.") end 442 | end) 443 | end 444 | }) 445 | 446 | 447 | keymap.add { 448 | ["up"] = "plugin-manager:select-prev", 449 | ["down"] = "plugin-manager:select-next", 450 | ["pagedown"] = "plugin-manager:scroll-page-down", 451 | ["pageup"] = "plugin-manager:scroll-page-up", 452 | ["home"] = "plugin-manager:scroll-page-top", 453 | ["end"] = "plugin-manager:scroll-page-bottom", 454 | ["lclick"] = "plugin-manager:select", 455 | ["ctrl+f"] = "plugin-manager:filter", 456 | ["ctrl+r"] = "plugin-manager:refresh-all", 457 | ["ctrl+u"] = "plugin-manager:upgrade-all", 458 | ["2lclick"] = { "plugin-manager:install-selected", "plugin-manager:uninstall-selected" }, 459 | ["return"] = { "plugin-manager:install-selected", "plugin-manager:uninstall-selected" }, 460 | ["escape"] = "plugin-manager:clear-filter", 461 | } 462 | 463 | 464 | PluginView.menu:register(function() return core.active_view:is(PluginView) end, { 465 | { text = "Install", command = "plugin-manager:install-hovered" }, 466 | { text = "Uninstall", command = "plugin-manager:uninstall-hovered" }, 467 | { text = "View Source", command = "plugin-manager:view-source-hovered" }, 468 | { text = "View README", command = "plugin-manager:view-readme-hovered" }, 469 | ContextMenu.DIVIDER, 470 | { text = "Refresh Listing", command = "plugin-manager:refresh-all" }, 471 | { text = "Upgrade All", command = "plugin-manager:upgrade-all" }, 472 | }) 473 | 474 | return PluginView 475 | -------------------------------------------------------------------------------- /plugins/welcome.lua: -------------------------------------------------------------------------------- 1 | -- mod-version:3 --lite-xl 2.1 2 | 3 | local core = require "core" 4 | local style = require "core.style" 5 | local command = require "core.command" 6 | local keymap = require "core.keymap" 7 | local View = require "core.view" 8 | local common = require "core.common" 9 | local EmptyView = require "core.emptyview" 10 | local Node = require "core.node" 11 | 12 | local PluginManager = require "plugins.plugin_manager" 13 | local PluginView = require "plugins.plugin_manager.plugin_view" 14 | 15 | 16 | local welcomed = system.get_file_info(USERDIR .. PATHSEP .. "welcomed") ~= nil 17 | if welcomed then return end 18 | 19 | local status, tv = pcall(require "plugins.treeview") 20 | if not status then command.perform("treeview:toggle") end 21 | 22 | local loading = nil 23 | 24 | local hovered_button = nil 25 | local function draw_button(view, x, y, w, button) 26 | local highlight = hovered_button == button 27 | if highlight then core.request_cursor("hand") end 28 | local button_height = style.font:get_height() + style.padding.y * 2 29 | local tw = common.draw_text(style.font, highlight and style.accent or style.text, button.label, "left", x, y, w, button_height) 30 | if tw < x + w then 31 | renderer.draw_rect(x + w, y - 3, button_height, button_height, highlight and style.dim or style.background2) 32 | renderer.draw_text(style.icon_font, "+", x + w + style.padding.x, y + style.padding.y, style.accent) 33 | end 34 | return x, y, w + button_height, button_height 35 | end 36 | 37 | local buttons = { 38 | { label = "Install Addons Package", command = "welcome:install-addons", tooltip = { 39 | "Installs syntax highlightings, themes, and plugins that make Lite XL easier to use.", 40 | "", 41 | "Recommended for newcomers to Lite XL.", 42 | "Requires a network connection." 43 | } }, 44 | { label = "Open Plugin Manager", command = "welcome:open-plugin-manager", tooltip = { "Manually select plugins you'd like to install before beginning with Lite XL.", "", "Requires a network connection." } }, 45 | { label = "Dismiss Welcome Options", command = "welcome:dismiss", tooltip = { "Dismisses this screen permanently." } } 46 | } 47 | 48 | local old_get_name = EmptyView.get_name 49 | function EmptyView:get_name() if welcomed then return old_get_name(self) end return "Welcome!" end 50 | 51 | local old_draw = EmptyView.draw 52 | function EmptyView:draw() 53 | if welcomed then return old_draw(self) end 54 | self:draw_background(style.background) 55 | if loading then 56 | local y = self.position.y + self.size.y / 2 57 | self:draw_background(style.background) 58 | PluginView.draw_loading_screen(self, loading.label, loading.percent) 59 | return 60 | end 61 | 62 | 63 | local title = "Lite XL" 64 | local version = "version " .. VERSION 65 | local title_width = style.big_font:get_width(title) 66 | local version_width = style.font:get_width(version) 67 | 68 | local button_width = math.min(self.size.x / 2 - 80, 300) 69 | 70 | local th = style.big_font:get_height() 71 | local dh = 2 * th + style.padding.y * #buttons 72 | local w = math.max(title_width, version_width) + button_width + style.padding.x * 2 + math.ceil(1*SCALE) 73 | local h = (style.font:get_height() + style.padding.y) * #buttons + style.padding.y + style.font:get_height() 74 | local x = self.position.x + math.max(style.padding.x, (self.size.x - w) / 2) 75 | local y = self.position.y + (self.size.y - h) / 2 76 | 77 | 78 | local x1, y1 = x, y + ((dh - th) / #buttons) 79 | local xv = x1 80 | if version_width > title_width then 81 | version = VERSION 82 | version_width = style.font:get_width(version) 83 | xv = x1 - (version_width - title_width) 84 | end 85 | x = renderer.draw_text(style.big_font, title, x1, y1, style.dim) 86 | renderer.draw_text(style.font, version, xv, y1 + th, style.dim) 87 | x = x + style.padding.x 88 | renderer.draw_rect(x, y, math.ceil(1 * SCALE), dh, style.dim) 89 | 90 | x = x + style.padding.x 91 | 92 | local button_height = style.padding.y * 2 + style.font:get_height() 93 | renderer.draw_rect(x, y, button_width, #buttons * (button_height + style.padding.y), style.background) 94 | for i,v in ipairs(buttons) do 95 | v.x, v.y, v.w, v.h = draw_button(self, x + style.padding.x, y, button_width, v) 96 | y = y + v.h + style.padding.y * 2 97 | end 98 | 99 | if hovered_button then 100 | for i, v in ipairs(hovered_button.tooltip) do 101 | common.draw_text(style.font, style.text, v, "center", self.position.x, y + style.padding.y, self.size.x, style.font:get_height()) 102 | y = y + style.font:get_height() 103 | end 104 | else 105 | common.draw_text(style.font, style.text, "Hover over one of the options above to get started.", "center", self.position.x, y + style.padding.y, self.size.x, style.font:get_height()) 106 | end 107 | end 108 | 109 | function EmptyView:on_mouse_moved(x, y) 110 | hovered_button = nil 111 | for i,v in ipairs(buttons) do 112 | if v.x and x >= v.x and x < v.x + v.w and y >= v.y and y < v.y + v.h then 113 | hovered_button = v 114 | end 115 | end 116 | end 117 | 118 | function EmptyView:on_mouse_pressed(button, x, y) 119 | if hovered_button and not welcomed then command.perform(hovered_button.command) end 120 | end 121 | 122 | 123 | 124 | local function terminate_welcome() 125 | io.open(USERDIR .. PATHSEP .. "welcomed", "wb"):close() 126 | command.perform("treeview:toggle") 127 | welcomed = true 128 | end 129 | 130 | command.add(EmptyView, { 131 | ["welcome:install-addons"] = function() 132 | core.log("Installing addons...") 133 | loading = { percent = 0, label = "Initializing..." } 134 | core.redraw = true 135 | PluginManager:install({ id = "meta_addons" }, { progress = function(progress) 136 | loading = progress 137 | core.redraw = true 138 | end, restart = false }):done(function() 139 | loading = false 140 | core.log("Addons installed!") 141 | terminate_welcome() 142 | command.perform("core:restart") 143 | end):fail(function(err) 144 | loading = false 145 | core.redraw = true 146 | core.error(err or "Error installing addons.") 147 | end) 148 | end, 149 | ["welcome:open-plugin-manager"] = function() 150 | command.perform("plugin-manager:show") 151 | end, 152 | ["welcome:dismiss"] = function() 153 | terminate_welcome() 154 | end 155 | }) 156 | 157 | -------------------------------------------------------------------------------- /t/run.lua: -------------------------------------------------------------------------------- 1 | local lpm 2 | local function assert_exists(path) if not io.open(path, "rb") then error("assertion failed: file " .. path .. " does not exist", 2) end end 3 | local function assert_not_exists(path) if io.open(path, "rb") then error("assertion failed: file " .. path .. " exists", 2) end end 4 | local tmpdir = (os.getenv("TMPDIR") or "/tmp") .. "/lpmtest" 5 | local fast = os.getenv("FAST") 6 | local userdir = tmpdir .. "/lpmtest/user" 7 | setmetatable(_G, { __index = function(t, k) if not rawget(t, k) then error("cannot get undefined global variable: " .. k, 2) end end, __newindex = function(t, k) error("cannot set global variable: " .. k, 2) end }) 8 | 9 | 10 | local tests = { 11 | ["00_install_singleton"] = function() 12 | local plugins = lpm("list bracketmatch")["addons"] 13 | assert(#plugins == 1) 14 | assert(plugins[1].organization == "singleton") 15 | assert(plugins[1].status == "available") 16 | local actions = lpm("install bracketmatch")["actions"] 17 | assert(actions[1]:find("Installing singleton")) 18 | assert_exists(userdir .. "/plugins/bracketmatch.lua") 19 | actions = lpm("uninstall bracketmatch")["actions"] 20 | assert_not_exists(userdir .. "/plugins/bracketmatch.lua") 21 | end, 22 | ["01_upgrade_singleton"] = function() 23 | lpm("install bracketmatch") 24 | local plugins = lpm("list bracketmatch")["addons"] 25 | assert(#plugins == 1) 26 | assert(plugins[1].status == "installed") 27 | assert_exists(plugins[1].path) 28 | io.open(plugins[1].path, "ab"):write("-- this is a test comment to modify the checksum"):close() 29 | plugins = lpm("list bracketmatch")["addons"] 30 | assert(#plugins == 2) 31 | lpm("install bracketmatch") 32 | plugins = lpm("list bracketmatch")["addons"] 33 | assert(#plugins == 1) 34 | lpm("install console") 35 | assert_exists(userdir .. "/plugins/console/init.lua") 36 | end, 37 | ["02_install_complex"] = function() 38 | local plugins = lpm("list plugin_manager")["addons"] 39 | assert(#plugins == 1) 40 | assert(plugins[1].organization == "complex") 41 | assert(plugins[1].status == "available") 42 | assert(plugins[1].dependencies.json) 43 | local actions = lpm("install plugin_manager")["actions"] 44 | assert_exists(userdir .. "/libraries/json.lua") 45 | assert_exists(userdir .. "/plugins/plugin_manager") 46 | assert_exists(userdir .. "/plugins/plugin_manager/init.lua") 47 | actions = lpm("uninstall plugin_manager")["actions"] 48 | assert_not_exists(userdir .. "/plugins/plugin_manager") 49 | lpm("install editorconfig") 50 | assert_exists(userdir .. "/plugins/editorconfig") 51 | assert_exists(userdir .. "/plugins/editorconfig/init.lua") 52 | end, 53 | ["03_upgrade_complex"] = function() 54 | local actions = lpm("install plugin_manager") 55 | local plugins = lpm("list plugin_manager")["addons"] 56 | assert(#plugins == 1) 57 | assert(plugins[1].organization == "complex") 58 | assert(plugins[1].status == "installed") 59 | end, 60 | ["04_list_plugins"] = function() 61 | local plugins = lpm("list")["addons"] 62 | assert(#plugins > 20) 63 | lpm("list --status core") 64 | end, 65 | -- ["05_install_url"] = function() 66 | -- local plugins = lpm("list language_ksy")["addons"] 67 | -- assert(#plugins == 1) 68 | -- assert(plugins[1].organization == "singleton") 69 | -- assert(plugins[1].status == "available") 70 | -- local actions = lpm("install language_ksy") 71 | -- assert_exists(userdir .. "/plugins/language_ksy.lua") 72 | -- end, 73 | ["06_install_stub"] = function() 74 | local plugins = lpm("list lsp")["addons"] 75 | assert(#plugins > 1) 76 | for i, plugin in ipairs(plugins) do 77 | if plugin.id == "lsp" then 78 | assert(plugins[1].organization == "complex") 79 | assert(plugins[1].status == "available") 80 | local actions = lpm("install lsp") 81 | assert_exists(userdir .. "/plugins/lsp/init.lua") 82 | assert_exists(userdir .. "/libraries/widget/init.lua") 83 | break 84 | end 85 | end 86 | local actions = lpm("install encodings") 87 | assert_exists(userdir .. "/plugins/encodings.lua") 88 | local stat = system.stat(userdir .. "/plugins/encodings.lua") 89 | assert(stat) 90 | assert(stat.type == "file") 91 | assert_exists(userdir .. "/libraries/encoding") 92 | stat = system.stat(userdir .. "/libraries/encoding") 93 | assert(stat) 94 | assert(stat.type == "dir") 95 | end, 96 | ["07_manifest"] = function() 97 | local results = json.decode(io.open("manifest.json", "rb"):read("*all")) 98 | assert(#results["remotes"] == 2) 99 | assert(#results["addons"] == 3) 100 | end, 101 | ["08_install_many"] = function() 102 | lpm("install encoding gitblame gitstatus language_ts lsp minimap") 103 | end, 104 | ["09_misc_commands"] = function() 105 | lpm("update") 106 | lpm("upgrade") 107 | end, 108 | ["10_install_multiarch"] = function() 109 | lpm("install plugin_manager --arch x86_64-windows --arch x86_64-linux") 110 | assert_exists(userdir .. "/plugins/plugin_manager/lpm.x86_64-linux") 111 | assert_exists(userdir .. "/plugins/plugin_manager/lpm.x86_64-windows.exe") 112 | assert_exists(userdir .. "/plugins/plugin_manager/init.lua") 113 | end, 114 | ["11_dependency_check"] = function() 115 | lpm("install lsp") 116 | assert_exists(userdir .. "/plugins/lsp") 117 | assert_exists(userdir .. "/plugins/lintplus") 118 | lpm("uninstall lsp") 119 | assert_not_exists(userdir .. "/plugins/lsp") 120 | assert_not_exists(userdir .. "/plugins/lintplus") 121 | end, 122 | ["12_masking"] = function() 123 | lpm("install lsp --mask lintplus") 124 | assert_exists(userdir .. "/plugins/lsp") 125 | assert_not_exists(userdir .. "/plugins/lintplus") 126 | end, 127 | ["13_repos"] = function() 128 | lpm("repo add https://github.com/jgmdev/lite-xl-threads.git") 129 | end 130 | } 131 | 132 | local last_command_result, last_command 133 | lpm = function(cmd) 134 | last_command = string.format("%s --quiet --json --assume-yes --mod-version=3 --userdir=%s --tmpdir=%s --cachedir=%s --configdir=%s %s", arg[0], userdir, tmpdir, tmpdir, tmpdir, cmd); 135 | local pipe = io.popen(last_command, "r") 136 | local result = pipe:read("*all") 137 | last_command_result = result ~= "" and json.decode(result) or nil 138 | local success = pipe:close() 139 | if not success then error("error calling lpm", 2) end 140 | return last_command_result 141 | end 142 | 143 | local function run_tests(tests, arg) 144 | local fail_count = 0 145 | local names = {} 146 | if #arg == 0 then 147 | for k,v in pairs(tests) do table.insert(names, k) end 148 | else 149 | names = arg 150 | end 151 | table.sort(names) 152 | local max_name = 0 153 | os.execute("rm -rf " .. tmpdir .. " && mkdir -p " .. tmpdir .. " " .. userdir); 154 | for i,k in ipairs(names) do max_name = math.max(max_name, #k) end 155 | for i,k in ipairs(names) do 156 | local v = tests[k] 157 | if fast then 158 | os.execute("rm -rf " .. tmpdir .. "/plugins && mkdir -p " .. tmpdir); 159 | else 160 | os.execute("rm -rf " .. tmpdir .. " && mkdir -p " .. tmpdir); 161 | end 162 | io.stdout:write(string.format("test %-" .. (max_name + 1) .. "s: ", k)) 163 | local failed = false 164 | xpcall(v, function(err) 165 | print("[FAIL]: " .. debug.traceback(err, 2)) 166 | if last_command then 167 | print("Last Command: " .. last_command) 168 | if last_command_result then 169 | print(json.encode(last_command_result)) 170 | end 171 | end 172 | print() 173 | print() 174 | fail_count = fail_count + 1 175 | failed = true 176 | end) 177 | if not failed then 178 | print("[PASSED]") 179 | end 180 | end 181 | io.stdout:write("\x1B[?25h") 182 | io.stdout:flush() 183 | os.exit(fail_count) 184 | end 185 | 186 | run_tests(tests, { ... }) 187 | --------------------------------------------------------------------------------