diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a06ed29 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = tab diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 371d284..f1fe1b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,8 +1,12 @@ name: Build on: - - push - - pull_request + push: + branches: + - master + pull_request: + branches: + - master jobs: build: @@ -19,18 +23,18 @@ jobs: goos: windows steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: true - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: '1.17.7' + go-version: '1.22.2' - name: Download Task run: 'sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d' - name: Build run: GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} ./bin/task - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: matrix.goos == 'windows' with: name: hilbish-${{ matrix.goos }}-${{ matrix.goarch }} @@ -44,7 +48,7 @@ jobs: libs docs emmyLuaDocs - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: matrix.goos != 'windows' with: name: hilbish-${{ matrix.goos }}-${{ matrix.goarch }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9d2728b..453430d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e378376..d524457 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,14 +2,15 @@ name: Generate docs on: push: - branches: [master] + branches: + - master jobs: gen: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 - name: Run docgen run: go run cmd/docgen/docgen.go - name: Commit new docs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a3a2840..f4606c3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: create-release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: taiki-e/create-gh-release-action@v1 with: title: Hilbish $tag @@ -30,13 +30,17 @@ jobs: - goarch: arm64 goos: windows steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: true - - uses: wangyoucao577/go-release-action@v1.25 + fetch-depth: 0 + - name: Download Task + run: 'sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d' + - uses: wangyoucao577/go-release-action@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} goos: ${{ matrix.goos }} goarch: ${{ matrix.goarch }} + ldflags: '-s -w' binary_name: hilbish extra_files: LICENSE README.md CHANGELOG.md .hilbishrc.lua nature libs docs emmyLuaDocs diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml new file mode 100644 index 0000000..4b9b8af --- /dev/null +++ b/.github/workflows/website.yml @@ -0,0 +1,51 @@ +name: Build website + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 0 + + - name: Setup Hugo + uses: peaceiris/actions-hugo@v3 + with: + hugo-version: '0.111.3' + extended: true + + - name: Set branch name + id: branch + run: echo "BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_ENV" + + - name: Fix base URL + if: env.BRANCH_NAME != 'master' && github.repository_owner == 'Rosettea' + run: sed -i "s%baseURL = 'https://rosettea.github.io/Hilbish/'%baseURL = 'https://rosettea.github.io/Hilbish/versions/${{ env.BRANCH_NAME }}'%" website/config.toml + + - name: Build + run: 'cd website && hugo --minify' + + - name: Deploy + if: env.BRANCH_NAME == 'master' && github.repository_owner == 'Rosettea' + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./website/public + keep_files: true + - name: Deploy + if: env.BRANCH_NAME != 'master' && github.repository_owner == 'Rosettea' + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./website/public + destination_dir: versions/${{ env.BRANCH_NAME }} + keep_files: true diff --git a/.gitignore b/.gitignore index 338ef97..1abf82c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ *.exe hilbish +!docs/api/hilbish docgen +!cmd/docgen .vim petals/ +.hugo_build.lock diff --git a/.hilbishrc.lua b/.hilbishrc.lua index 5d6382b..249f97e 100644 --- a/.hilbishrc.lua +++ b/.hilbishrc.lua @@ -1,18 +1,39 @@ -- Default Hilbish config +local hilbish = require 'hilbish' local lunacolors = require 'lunacolors' local bait = require 'bait' local ansikit = require 'ansikit' +local unreadCount = 0 +local running = false local function doPrompt(fail) hilbish.prompt(lunacolors.format( '{blue}%u {cyan}%d ' .. (fail and '{red}' or '{green}') .. '∆ ' )) end +local function doNotifyPrompt() + if running or unreadCount == hilbish.messages.unreadCount() then return end + + local notifPrompt = string.format('• %s unread notification%s', hilbish.messages.unreadCount(), hilbish.messages.unreadCount() > 1 and 's' or '') + unreadCount = hilbish.messages.unreadCount() + hilbish.prompt(lunacolors.blue(notifPrompt), 'right') + + hilbish.timeout(function() + hilbish.prompt('', 'right') + end, 3000) +end + doPrompt() +bait.catch('command.preexec', function() + running = true +end) + bait.catch('command.exit', function(code) + running = false doPrompt(code ~= 0) + doNotifyPrompt() end) bait.catch('hilbish.vimMode', function(mode) @@ -22,3 +43,7 @@ bait.catch('hilbish.vimMode', function(mode) ansikit.cursorStyle(ansikit.lineCursor) end end) + +bait.catch('hilbish.notification', function(notif) + doNotifyPrompt() +end) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49904ad..c4db0c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,153 @@ # 🎀 Changelog ## Unreleased -**NOTE:** Hilbish now uses [Task] insead of Make for builds. +### Added +- `fs.pipe` function to get a pair of connected files (a pipe). +- Added an alternative 2nd parameter to `hilbish.run`, which is `streams`. +`streams` is a table of input and output streams to run the command with. +It uses these 3 keys: + - `input` as standard input for the command + - `out` as standard output + - `err` as standard error + +Here is a minimal example of the new usage which allows users to now pipe commands +directly via Lua functions: + +```lua +local fs = require 'fs' +local pr, pw = fs.pipe() +hilbish.run('ls -l', { + stdout = pw, + stderr = pw, +}) + +pw:close() + +hilbish.run('wc -l', { + stdin = pr +}) +``` + +### Fixed +- Fix ansi attributes causing issues with text when cut off in greenhouse +- `exec` command should return if no arg presented + +## [2.2.3] - 2024-04-27 +### Fixed +- Highligher and hinter work now, since it was regressed from the previous minor release. +- `cat` command no longer prints extra newline at end of each file + +### Added +- `cat` command now reads files in chunks, allowing for reading large files + +## [2.2.2] - 2024-04-16 +### Fixed +- Line refresh fixes (less flicker) +- Do more checks for a TTY + - Panic if ENOTTY is thrown from readline + - use `x/term` function to check if a terminal + +### Added +- Page Up/Down keybinds for Greenhouse will now scroll up and down the size of the region (a page) + +### Changed +- Remove usage of `hilbish.goro` in Greenhouse. +- Values in `hilbish` table are no longer protected. This means +they can be overridden. (#287) + +## [2.2.1] - 2023-12-26 +### Fixed +- Removed a left over debug print +- Recover panic in `hilbish.goro` + +## [2.2.0] - 2023-12-25 +### Added +- [Native Modules](https://rosettea.github.io/Hilbish/docs/api/hilbish/hilbish.module/) +- Made a few additions to the sink type: + - `read()` method for retrieving input (so now the `in` sink of commanders is useful) + - `flush()` and `autoFlush()` related to flushing outputs + - `pipe` property to check if a sink with input is a pipe (like stdin) +- Add fuzzy search to history search (enable via `hilbish.opts.fuzzy = true`) +- Show indexes on cdr list and use ~ for home directory. +- Fix doc command not displaying correct subdocs when using shorthand api doc access (`doc api hilbish.jobs` as an example) +- `hilbish.messages` interface (details in [#219]) +- `hilbish.notification` signal when a message/notification is sent +- `notifyJobFinish` opt to send a notification when background jobs are +- `hilbish.goVersion` for the version of Go used to compile Hilbish. +completed. +- Allow numbered arg substitutions in aliases. + - Example: `hilbish.alias('hello', 'echo %1 says hello')` allows the user to run `hello hilbish` + which will output `hilbish says hello`. +- Greenhouse + - Greenhouse is a pager library and program. Basic usage is `greenhouse ` + - Using this also brings enhancements to the `doc` command like easy + navigation of neighboring doc files. + Ctrl-N can be used for the table of contents, which views adjacent documentation. + +### Changed +- Documentation for EVERYTHING has been improved, with more +information added, code example, parameter details, etc. +You can see the improvements! +- Documentation has gotten an uplift in the `doc` command. +This includes: + - Proper highlighting of code + - Paging (via Greenhouse) + - Highlighting more markdown things + +### Fixed +- Fix panic when runner doesn't return a table +- Fix edge case of crash on empty alias resolve +- File completion on Windows +- Job management commands work now +- Fix infinite loop when navigating history without any history. [#252](https://github.com/Rosettea/Hilbish/issues/252) +- Return the prefix when calling `hilbish.completions.call`. [#219](https://github.com/Rosettea/Hilbish/issues/219) +- Replaced `sed` in-place editing with `grep` and `mv` for compatibility with BSD utils + +## [2.1.2] - 2022-04-10 +### Removed +- Bad april fools code ;( + +## [2.1.1] - 2022-04-01 +### Added +- Validation checks for command input +- Improved runtime performance +- Validate Lua code + +## [2.1.0] - 2022-02-10 +### Added +- Documented custom userdata types (Job and Timer Objects) + - Coming with this fix is also adding the return types for some functions that were missing it +- Added a dedicated input and dedicated outputs for commanders (sinks - info at `doc api commander`). +- Local docs is used if one of Hilbish's branches is found +- Return 1 exit code on doc not found +- `hilbish.runner.getCurrent()` to get the current runner +- Initialize Hilbish Lua API before handling signals + +### Fixed +- `index` or `_index` subdocs should not show up anymore +- `hilbish.which` not working correctly with aliases +- Commanders not being able to pipe with commands or any related operator. +- Resolve symlinks in completions +- Updated `runner-mode` docs +- Fix `hilbish.completion` functions panicking when empty input is provided + +## [2.0.1] - 2022-12-28 +### Fixed +- Corrected documentation for hooks, removing outdated `command.no-perm` +- Fixed an issue where `cd` with no args would not update the old pwd +- Tiny documentation enhancements for the `hilbish.timer` interface + +## [2.0.0] - 2022-12-20 +**NOTES FOR USERS/PACKAGERS UPDATING:** +- Hilbish now uses [Task] insead of Make for builds. +- The doc format has been changed from plain text to markdown. +**YOU MUST reinstall Hilbish to remove the duplicate, old docs.** +- Hilbish will by default install to **`/usr/local`** instead of just `/usr/` +when building via Task. This is mainly to avoid conflict of distro packages +and local installs, and is the correct place when building from git either way. +To keep Hilbish in `/usr`, you must have `PREFIX="/usr/"` when running `task build` or `task install` +- Windows is no longer supported. It will build and run, but **will** have problems. +If you want to help fix the situation, start a discussion or open an issue and contribute. [Task]: https://taskfile.dev/#/ @@ -39,7 +185,7 @@ without arguments will disown the last job. fields on a job object. - Documentation for jobs is now available via `doc jobs`. - `hilbish.alias.resolve(cmdstr)` to resolve a command alias. -- `hilbish.opts` for shell options. Currently, the only opt is `autocd`. +- `hilbish.opts` for shell options. - `hilbish.editor` interface for interacting with the line editor that Hilbish uses. - `hilbish.vim` interface to dynamically get/set vim registers. @@ -73,12 +219,20 @@ disables commands being added to history. - A new and "safer" event emitter has been added. This causes a performance deficit, but avoids a lot of random errors introduced with the new Lua runtime (see [#197]) - `bait.release(name, catcher)` removes `handler` for the named `event` +- `exec`, `clear` and `cat` builtin commands +- `hilbish.cancel` hook thrown when user cancels input with Ctrl-C +- 1st item on history is now inserted when history search menu is opened ([#148]) +- Documentation has been improved vastly! +[#148]: https://github.com/Rosettea/Hilbish/issues/148 [#197]: https://github.com/Rosettea/Hilbish/issues/197 ### Changed - **Breaking Change:** Upgraded to Lua 5.4. This is probably one of (if not the) biggest things in this release. +To recap quickly on what matters (mostly): + - `os.execute` returns 3 values instead of 1 (but you should be using `hilbish.run`) + - I/O operations must be flushed (`io.flush()`) - **Breaking Change:** MacOS config paths now match Linux. - Overrides on the `hilbish` table are no longer permitted. - **Breaking Change:** Runner functions are now required to return a table. @@ -97,6 +251,7 @@ of a dot. (ie. `job.stop()` -> `job:stop()`) - All `fs` module functions which take paths now implicitly expand ~ to home. - **Breaking Change:** `hilbish.greeting` has been moved to an opt (`hilbish.opts.greeting`) and is always printed by default. To disable it, set the opt to false. +- **Breaking Change:** `command.no-perm` hook has been replaced with `command.not-executable` - History is now fetched from Lua, which means users can override `hilbish.history` methods to make it act how they want. - `guide` has been removed. See the [website](https://rosettea.github.io/Hilbish/) @@ -112,7 +267,7 @@ replacing the last character. - `hilbish.login` being the wrong value. - Put full input in history if prompted for continued input - Don't put alias expanded command in history (sound familiar?) -- Handle cases of stdin being nonblocking (in the case of [#130](https://github.com/Rosettea/Hilbish/issues/130)) +- Handle cases of stdin being nonblocking (in the case of [#136](https://github.com/Rosettea/Hilbish/issues/136)) - Don't prompt for continued input if non interactive - Don't insert unhandled control keys. - Handle sh syntax error in alias @@ -122,11 +277,12 @@ certain color rules. - Home/End keys now go to the actual start/end of the input. - Input getting cut off on enter in certain cases. - Go to the next line properly if input reaches end of terminal width. -- Cursor position with CJK characters. ([#145](https://github.com/Rosettea/Hilbish/pull/145)) -- Files with same name as parent folder in completions getting cut off [#136](https://github.com/Rosettea/Hilbish/issues/136)) +- Cursor position with CJK characters has been corrected ([#145](https://github.com/Rosettea/Hilbish/pull/145)) +- Files with same name as parent folder in completions getting cut off [#130](https://github.com/Rosettea/Hilbish/issues/130)) - `hilbish.which` now works with commanders and aliases. - Background jobs no longer take stdin so they do not interfere with shell input. +- Full name of completion entry is used instead of being cut off - Completions are fixed in cases where the query/line is an alias alone where it can also resolve to the beginning of command names. (reference [this commit](https://github.com/Rosettea/Hilbish/commit/2790982ad123115c6ddbc5764677fdca27668cea)) @@ -145,6 +301,29 @@ menu is open. - Escape codes now work. - Escape percentage symbols in completion entries, so you will no longer see an error of missing format variable +- Fix an error with sh syntax in aliases +- Prompt now works with east asian characters (CJK) +- Set back the prompt to normal after exiting the continue prompt with ctrl-d +- Take into account newline in input when calculating input width. Prevents +extra reprinting of the prompt, but input with newlines inserted is still a problem +- Put cursor at the end of input when exiting $EDITOR with Vim mode bind +- Calculate width of virtual input properly (completion candidates) +- Users can now tab complete files with spaces while quoted or with escaped spaces. +This means a query of `Files\ to\ ` with file names of `Files to tab complete` and `Files to complete` +will result in the files being completed. +- Fixed grid menu display if cell width ends up being the width of the terminal +- Cut off item names in grid menu if its longer than cell width +- Fix completion search menu disappearing +- Make binary completion work with bins that have spaces in the name +- Completion paths having duplicated characters if it's escaped +- Get custom completion command properly to call from Lua +- Put proper command on the line when using up and down arrow keys to go through command history +- Don't do anything if length of input rune slice is 0 ([commit for explanation](https://github.com/Rosettea/Hilbish/commit/8d40179a73fe5942707cd43f9c0463dee53eedd8)) + +## [2.0.0-rc1] - 2022-09-14 +This is a pre-release version of Hilbish for testing. To see the changelog, +refer to the `Unreleased` section of the [full changelog](CHANGELOG.md) +(version 2.0.0 for future reference). ## [1.2.0] - 2022-03-17 ### Added @@ -569,6 +748,17 @@ This input for example will prompt for more input to complete: First "stable" release of Hilbish. +[2.2.3]: https://github.com/Rosettea/Hilbish/compare/v2.2.2...v2.2.3 +[2.2.2]: https://github.com/Rosettea/Hilbish/compare/v2.2.1...v2.2.2 +[2.2.1]: https://github.com/Rosettea/Hilbish/compare/v2.2.0...v2.2.1 +[2.2.0]: https://github.com/Rosettea/Hilbish/compare/v2.1.0...v2.2.0 +[2.1.2]: https://github.com/Rosettea/Hilbish/compare/v2.1.1...v2.1.2 +[2.1.1]: https://github.com/Rosettea/Hilbish/compare/v2.1.0...v2.1.1 +[2.1.0]: https://github.com/Rosettea/Hilbish/compare/v2.0.1...v2.1.0 +[2.0.1]: https://github.com/Rosettea/Hilbish/compare/v2.0.0...v2.0.1 +[2.0.0]: https://github.com/Rosettea/Hilbish/compare/v1.2.0...v2.0.0 +[2.0.0-rc1]: https://github.com/Rosettea/Hilbish/compare/v1.2.0...v2.0.0-rc1 +[1.2.0]: https://github.com/Rosettea/Hilbish/compare/v1.1.4...v1.2.0 [1.1.0]: https://github.com/Rosettea/Hilbish/compare/v1.0.4...v1.1.0 [1.0.4]: https://github.com/Rosettea/Hilbish/compare/v1.0.3...v1.0.4 [1.0.3]: https://github.com/Rosettea/Hilbish/compare/v1.0.2...v1.0.3 diff --git a/LICENSE b/LICENSE index da3c8c1..3d8f013 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Rosettea +Copyright (c) 2021-2023 Rosettea Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 86879d1..9d6446c 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,45 @@ -
-
-
-
- 🌺 The flower shell. A comfy and nice little shell for Lua fans! -
-

- GitHub commit activity - GitHub commits since latest release (by date) - GitHub contributors
- help wanted - GitHub license - Discord -

-
+
+
+🌓 The Moon-powered shell! A comfy and extensible shell for Lua fans! 🌺 ✨ +
-Hilbish is a extensible shell (framework). It was made to be very customizable -via the Lua programming language. It aims to be easy to use for the casual -people but powerful for those who want to tinker more with their shell, -the thing used to interface with most of the system. +GitHub commit activityGitHub commits since latest release (by date)GitHub contributors
+help wanted +GitHub license +Discord +
+ +Hilbish is an extensible shell designed to be highly customizable. +It is configured in Lua and provides a good range of features. +It aims to be easy to use for anyone but powerful enough for +those who need it. The motivation for choosing Lua was that its simpler and better to use than old shell script. It's fine for basic interactive shell uses, but that's the only place Hilbish has shell script; everything else is Lua and aims to be infinitely configurable. If something isn't, open an issue! -# Table of Contents -- [Screenshots](#Screenshots) -- [Installation](#Installation) - - [Prebuilt Bins](#Prebuilt-binaries) - - [AUR](#AUR) - - [Nixpkgs](#Nixpkgs) - - [Manual Build](#Manual-Build) -- [Getting Started](#Getting-Started) -- [Contributing](#Contributing) - # Screenshots
-

-

+
-# Installation -## Prebuilt binaries -Go [here](https://nightly.link/Rosettea/Hilbish/workflows/build/master) for -builds on the master branch. +# Getting Hilbish +**NOTE:** Hilbish is not guaranteed to work properly on Windows, starting +from the 2.0 version. It will still be able to compile, but functionality +may be lacking. If you want to contribute to make the situation better, +comment on the Windows discussion. -## AUR -[![AUR maintainer](https://img.shields.io/aur/maintainer/hilbish?logo=arch-linux&style=flat-square)](https://aur.archlinux.org/packages/hilbish) -Arch Linux users can install Hilbish from the AUR with the following command: -```sh -yay -S hilbish -``` +You can check the [install page](https://rosettea.github.io/Hilbish/install/) +on the website for distributed binaries from GitHub or other package repositories. +Otherwise, continue reading for steps on compiling. -[![AUR maintainer](https://img.shields.io/aur/maintainer/hilbish?logo=arch-linux&style=flat-square)](https://aur.archlinux.org/packages/hilbish-git) -Or from the latest `master` commit with: -```sh -yay -S hilbish-git -``` +## Prerequisites +- [Go 1.22+](https://go.dev) +- [Task](https://taskfile.dev/installation/) (**Go on the hyperlink here to see Task's install method for your OS.**) -## Nixpkgs -Nix/NixOS users can install Hilbish from the central repository, nixpkgs, through the usual ways. -If you're new to nix you should probably read up on how to do that [here](https://nixos.wiki/wiki/Cheatsheet). - -## Manual Build -### Prerequisites -- [Go 1.17+](https://go.dev) -- [Task](https://taskfile.dev/#/) - -### Build +## Build First, clone Hilbish. The recursive is required, as some Lua libraries are submodules. ```sh diff --git a/Taskfile.yaml b/Taskfile.yaml index 067f2ba..264e7d5 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -3,24 +3,35 @@ version: '3' vars: - PREFIX: '{{default "/usr" .PREFIX}}' + PREFIX: '{{default "/usr/local" .PREFIX}}' bindir__: '{{.PREFIX}}/bin' BINDIR: '{{default .bindir__ .BINDIR}}' libdir__: '{{.PREFIX}}/share/hilbish' LIBDIR: '{{default .libdir__ .LIBDIR}}' - GOFLAGS: '-ldflags "-s -w"' + goflags__: '-ldflags "-s -w -X main.dataDir={{.LIBDIR}}"' + GOFLAGS: '{{default .goflags__ .GOFLAGS}}' tasks: default: cmds: - go build {{.GOFLAGS}} vars: - GOFLAGS: '-ldflags "-s -w -X main.gitCommit=$(git rev-parse --short HEAD) -X main.gitBranch=$(git rev-parse --abbrev-ref HEAD)"' + GOFLAGS: '-ldflags "-s -w -X main.dataDir={{.LIBDIR}} -X main.gitCommit=$(git rev-parse --short HEAD) -X main.gitBranch=$(git rev-parse --abbrev-ref HEAD)"' + + default-nocgo: + cmds: + - CGO_ENABLED=0 go build {{.GOFLAGS}} + vars: + GOFLAGS: '-ldflags "-s -w -X main.dataDir={{.LIBDIR}} -X main.gitCommit=$(git rev-parse --short HEAD) -X main.gitBranch=$(git rev-parse --abbrev-ref HEAD)"' build: cmds: - go build {{.GOFLAGS}} + build-nocgo: + cmds: + - CGO_ENABLED=0 go build {{.GOFLAGS}} + install: cmds: - install -v -d "{{.DESTDIR}}{{.BINDIR}}/" && install -m 0755 -v hilbish "{{.DESTDIR}}{{.BINDIR}}/hilbish" @@ -33,4 +44,4 @@ tasks: - rm -vrf "{{.DESTDIR}}{{.BINDIR}}/hilbish" "{{.DESTDIR}}{{.LIBDIR}}" - - sed -i '/hilbish/d' /etc/shells + - grep -v 'hilbish' /etc/shells > /tmp/shells.hilbish_uninstall && mv /tmp/shells.hilbish_uninstall /etc/shells diff --git a/aliases.go b/aliases.go index 3007cc3..8c90fe5 100644 --- a/aliases.go +++ b/aliases.go @@ -1,6 +1,8 @@ package main import ( + "regexp" + "strconv" "strings" "sync" @@ -9,46 +11,69 @@ import ( rt "github.com/arnodel/golua/runtime" ) -var aliases *aliasHandler +var aliases *aliasModule -type aliasHandler struct { +type aliasModule struct { aliases map[string]string mu *sync.RWMutex } // initialize aliases map -func newAliases() *aliasHandler { - return &aliasHandler{ +func newAliases() *aliasModule { + return &aliasModule{ aliases: make(map[string]string), mu: &sync.RWMutex{}, } } -func (a *aliasHandler) Add(alias, cmd string) { +func (a *aliasModule) Add(alias, cmd string) { a.mu.Lock() defer a.mu.Unlock() a.aliases[alias] = cmd } -func (a *aliasHandler) All() map[string]string { +func (a *aliasModule) All() map[string]string { return a.aliases } -func (a *aliasHandler) Delete(alias string) { +func (a *aliasModule) Delete(alias string) { a.mu.Lock() defer a.mu.Unlock() delete(a.aliases, alias) } -func (a *aliasHandler) Resolve(cmdstr string) string { +func (a *aliasModule) Resolve(cmdstr string) string { a.mu.RLock() defer a.mu.RUnlock() - args := strings.Split(cmdstr, " ") + arg, _ := regexp.Compile(`[\\]?%\d+`) + + args, _ := splitInput(cmdstr) + if len(args) == 0 { + // this shouldnt reach but...???? + return cmdstr + } + for a.aliases[args[0]] != "" { alias := a.aliases[args[0]] + alias = arg.ReplaceAllStringFunc(alias, func(a string) string { + idx, _ := strconv.Atoi(a[1:]) + if strings.HasPrefix(a, "\\") || idx == 0 { + return strings.TrimPrefix(a, "\\") + } + + if idx + 1 > len(args) { + return a + } + val := args[idx] + args = cut(args, idx) + cmdstr = strings.Join(args, " ") + + return val + }) + cmdstr = alias + strings.TrimPrefix(cmdstr, args[0]) cmdArgs, _ := splitInput(cmdstr) args = cmdArgs @@ -66,7 +91,10 @@ func (a *aliasHandler) Resolve(cmdstr string) string { // lua section -func (a *aliasHandler) Loader(rtm *rt.Runtime) *rt.Table { +// #interface aliases +// command aliasing +// The alias interface deals with all command aliases in Hilbish. +func (a *aliasModule) Loader(rtm *rt.Runtime) *rt.Table { // create a lua module with our functions hshaliasesLua := map[string]util.LuaExport{ "add": util.LuaExport{hlalias, 2, false}, @@ -81,7 +109,26 @@ func (a *aliasHandler) Loader(rtm *rt.Runtime) *rt.Table { return mod } -func (a *aliasHandler) luaList(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +// #interface aliases +// add(alias, cmd) +// This is an alias (ha) for the [hilbish.alias](../#alias) function. +// --- @param alias string +// --- @param cmd string +func _hlalias() {} + +// #interface aliases +// list() -> table[string, string] +// Get a table of all aliases, with string keys as the alias and the value as the command. +// #returns table[string, string] +/* +#example +hilbish.aliases.add('hi', 'echo hi') + +local aliases = hilbish.aliases.list() +-- -> {hi = 'echo hi'} +#example +*/ +func (a *aliasModule) luaList(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { aliasesList := rt.NewTable() for k, v := range a.All() { aliasesList.Set(rt.StringValue(k), rt.StringValue(v)) @@ -90,7 +137,11 @@ func (a *aliasHandler) luaList(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext1(t.Runtime, rt.TableValue(aliasesList)), nil } -func (a *aliasHandler) luaDelete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +// #interface aliases +// delete(name) +// Removes an alias. +// #param name string +func (a *aliasModule) luaDelete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } @@ -103,7 +154,12 @@ func (a *aliasHandler) luaDelete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } -func (a *aliasHandler) luaResolve(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +// #interface aliases +// resolve(alias) -> string? +// Resolves an alias to its original command. Will thrown an error if the alias doesn't exist. +// #param alias string +// #returns string +func (a *aliasModule) luaResolve(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } diff --git a/api.go b/api.go index d060597..43e361a 100644 --- a/api.go +++ b/api.go @@ -1,12 +1,22 @@ -// Here is the core api for the hilbi shell itself -// Basically, stuff about the shell itself and other functions -// go here. +// the core Hilbish API +// The Hilbish module includes the core API, containing +// interfaces and functions which directly relate to shell functionality. +// #field ver The version of Hilbish +// #field goVersion The version of Go that Hilbish was compiled with +// #field user Username of the user +// #field host Hostname of the machine +// #field dataDir Directory for Hilbish data files, including the docs and default modules +// #field interactive Is Hilbish in an interactive shell? +// #field login Is Hilbish the login shell? +// #field vimMode Current Vim input mode of Hilbish (will be nil if not in Vim input mode) +// #field exitCode Exit code of the last executed command package main import ( "bytes" "errors" "fmt" + "io" "os" "os/exec" "runtime" @@ -18,8 +28,8 @@ import ( rt "github.com/arnodel/golua/runtime" "github.com/arnodel/golua/lib/packagelib" + "github.com/arnodel/golua/lib/iolib" "github.com/maxlandon/readline" - "github.com/blackfireio/osinfo" "mvdan.cc/sh/v3/interp" ) @@ -51,47 +61,8 @@ var hilbishLoader = packagelib.Loader{ } func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) { - fakeMod := rt.NewTable() - modmt := rt.NewTable() mod := rt.NewTable() - modIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - arg := c.Arg(1) - val := mod.Get(arg) - - return c.PushingNext1(t.Runtime, val), nil - } - modNewIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - k, err := c.StringArg(1) - if err != nil { - return nil, err - } - - v := c.Arg(2) - if k == "highlighter" { - var err error - // fine to assign, since itll be either nil or a closure - highlighter, err = c.ClosureArg(2) - if err != nil { - return nil, errors.New("hilbish.highlighter has to be a function") - } - } else if k == "hinter" { - var err error - hinter, err = c.ClosureArg(2) - if err != nil { - return nil, errors.New("hilbish.hinter has to be a function") - } - } else if modVal := mod.Get(rt.StringValue(k)); modVal != rt.NilValue { - return nil, errors.New("not allowed to override in hilbish table") - } - mod.Set(rt.StringValue(k), v) - - return c.Next(), nil - } - modmt.Set(rt.StringValue("__newindex"), rt.FunctionValue(rt.NewGoFunction(modNewIndex, "__newindex", 3, false))) - modmt.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(modIndex, "__index", 2, false))) - fakeMod.SetMetatable(modmt) - util.SetExports(rtm, mod, exports) hshMod = mod @@ -102,81 +73,68 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) { username = strings.Split(username, "\\")[1] // for some reason Username includes the hostname on windows } - util.SetFieldProtected(fakeMod, mod, "ver", rt.StringValue(getVersion()), "Hilbish version") - util.SetFieldProtected(fakeMod, mod, "user", rt.StringValue(username), "Username of user") - util.SetFieldProtected(fakeMod, mod, "host", rt.StringValue(host), "Host name of the machine") - util.SetFieldProtected(fakeMod, mod, "home", rt.StringValue(curuser.HomeDir), "Home directory of the user") - util.SetFieldProtected(fakeMod, mod, "dataDir", rt.StringValue(dataDir), "Directory for Hilbish's data files") - util.SetFieldProtected(fakeMod, mod, "interactive", rt.BoolValue(interactive), "If this is an interactive shell") - util.SetFieldProtected(fakeMod, mod, "login", rt.BoolValue(login), "Whether this is a login shell") - util.SetFieldProtected(fakeMod, mod, "vimMode", rt.NilValue, "Current Vim mode of Hilbish (nil if not in Vim mode)") - util.SetFieldProtected(fakeMod, mod, "exitCode", rt.IntValue(0), "Exit code of last exected command") - util.Document(fakeMod, "Hilbish's core API, containing submodules and functions which relate to the shell itself.") + util.SetField(rtm, mod, "ver", rt.StringValue(getVersion())) + util.SetField(rtm, mod, "goVersion", rt.StringValue(runtime.Version())) + util.SetField(rtm, mod, "user", rt.StringValue(username)) + util.SetField(rtm, mod, "host", rt.StringValue(host)) + util.SetField(rtm, mod, "home", rt.StringValue(curuser.HomeDir)) + util.SetField(rtm, mod, "dataDir", rt.StringValue(dataDir)) + util.SetField(rtm, mod, "interactive", rt.BoolValue(interactive)) + util.SetField(rtm, mod, "login", rt.BoolValue(login)) + util.SetField(rtm, mod, "vimMode", rt.NilValue) + util.SetField(rtm, mod, "exitCode", rt.IntValue(0)) // hilbish.userDir table - hshuser := rt.NewTable() - - util.SetField(rtm, hshuser, "config", rt.StringValue(confDir), "User's config directory") - util.SetField(rtm, hshuser, "data", rt.StringValue(userDataDir), "XDG data directory") - util.Document(hshuser, "User directories to store configs and/or modules.") + hshuser := userDirLoader(rtm) mod.Set(rt.StringValue("userDir"), rt.TableValue(hshuser)) // hilbish.os table - hshos := rt.NewTable() - info, _ := osinfo.GetOSInfo() - - util.SetField(rtm, hshos, "family", rt.StringValue(info.Family), "Family name of the current OS") - util.SetField(rtm, hshos, "name", rt.StringValue(info.Name), "Pretty name of the current OS") - util.SetField(rtm, hshos, "version", rt.StringValue(info.Version), "Version of the current OS") - util.Document(hshos, "OS info interface") + hshos := hshosLoader(rtm) mod.Set(rt.StringValue("os"), rt.TableValue(hshos)) // hilbish.aliases table aliases = newAliases() aliasesModule := aliases.Loader(rtm) - util.Document(aliasesModule, "Alias inferface for Hilbish.") mod.Set(rt.StringValue("aliases"), rt.TableValue(aliasesModule)) // hilbish.history table historyModule := lr.Loader(rtm) mod.Set(rt.StringValue("history"), rt.TableValue(historyModule)) - util.Document(historyModule, "History interface for Hilbish.") // hilbish.completion table hshcomp := completionLoader(rtm) - util.Document(hshcomp, "Completions interface for Hilbish.") + // TODO: REMOVE "completion" AND ONLY USE "completions" WITH AN S mod.Set(rt.StringValue("completion"), rt.TableValue(hshcomp)) + mod.Set(rt.StringValue("completions"), rt.TableValue(hshcomp)) // hilbish.runner table runnerModule := runnerModeLoader(rtm) - util.Document(runnerModule, "Runner/exec interface for Hilbish.") mod.Set(rt.StringValue("runner"), rt.TableValue(runnerModule)) // hilbish.jobs table jobs = newJobHandler() jobModule := jobs.loader(rtm) - util.Document(jobModule, "(Background) job interface.") mod.Set(rt.StringValue("jobs"), rt.TableValue(jobModule)) // hilbish.timers table - timers = newTimerHandler() - timerModule := timers.loader(rtm) - util.Document(timerModule, "Timer interface, for control of all intervals and timeouts.") - mod.Set(rt.StringValue("timers"), rt.TableValue(timerModule)) + timers = newTimersModule() + timersModule := timers.loader(rtm) + mod.Set(rt.StringValue("timers"), rt.TableValue(timersModule)) editorModule := editorLoader(rtm) - util.Document(editorModule, "") mod.Set(rt.StringValue("editor"), rt.TableValue(editorModule)) versionModule := rt.NewTable() - util.SetField(rtm, versionModule, "branch", rt.StringValue(gitBranch), "Git branch Hilbish was compiled from") - util.SetField(rtm, versionModule, "full", rt.StringValue(getVersion()), "Full version info, including release name") - util.SetField(rtm, versionModule, "commit", rt.StringValue(gitCommit), "Git commit Hilbish was compiled from") - util.SetField(rtm, versionModule, "release", rt.StringValue(releaseName), "Release name") - util.Document(versionModule, "Version info interface.") + util.SetField(rtm, versionModule, "branch", rt.StringValue(gitBranch)) + util.SetField(rtm, versionModule, "full", rt.StringValue(getVersion())) + util.SetField(rtm, versionModule, "commit", rt.StringValue(gitCommit)) + util.SetField(rtm, versionModule, "release", rt.StringValue(releaseName)) mod.Set(rt.StringValue("version"), rt.TableValue(versionModule)) - return rt.TableValue(fakeMod), nil + pluginModule := moduleLoader(rtm) + mod.Set(rt.StringValue("module"), rt.TableValue(pluginModule)) + + return rt.TableValue(mod), nil } func getenv(key, fallback string) string { @@ -188,20 +146,72 @@ func getenv(key, fallback string) string { } func setVimMode(mode string) { - util.SetField(l, hshMod, "vimMode", rt.StringValue(mode), "Current Vim mode of Hilbish (nil if not in Vim mode)") + util.SetField(l, hshMod, "vimMode", rt.StringValue(mode)) hooks.Emit("hilbish.vimMode", mode) } func unsetVimMode() { - util.SetField(l, hshMod, "vimMode", rt.NilValue, "Current Vim mode of Hilbish (nil if not in Vim mode)") + util.SetField(l, hshMod, "vimMode", rt.NilValue) } -// run(cmd, returnOut) -> exitCode, stdout, stderr -// Runs `cmd` in Hilbish's sh interpreter. -// If returnOut is true, the outputs of `cmd` will be returned as the 2nd and -// 3rd values instead of being outputted to the terminal. -// --- @param cmd string +func handleStream(v rt.Value, strms *streams, errStream bool) error { + ud, ok := v.TryUserData() + if !ok { + return errors.New("expected metatable argument") + } + + val := ud.Value() + var varstrm io.Writer + if f, ok := val.(*iolib.File); ok { + varstrm = f.Handle() + } + + if f, ok := val.(*sink); ok { + varstrm = f.writer + } + + if varstrm == nil { + return errors.New("expected either a sink or file") + } + + if errStream { + strms.stderr = varstrm + } else { + strms.stdout = varstrm + } + + return nil +} + +// run(cmd, streams) -> exitCode (number), stdout (string), stderr (string) +// Runs `cmd` in Hilbish's shell script interpreter. +// The `streams` parameter specifies the output and input streams the command should use. +// For example, to write command output to a sink. +// As a table, the caller can directly specify the standard output, error, and input +// streams of the command with the table keys `out`, `err`, and `input` respectively. +// As a boolean, it specifies whether the command should use standard output or return its output streams. +// #param cmd string +// #param streams table|boolean +// #returns number, string, string +// #example +/* +// This code is the same as `ls -l | wc -l` +local fs = require 'fs' +local pr, pw = fs.pipe() +hilbish.run('ls -l', { + stdout = pw, + stderr = pw, +}) + +pw:close() + +hilbish.run('wc -l', { + stdin = pr +}) +*/ +// #example func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + // TODO: ON BREAKING RELEASE, DO NOT ACCEPT `streams` AS A BOOLEAN. if err := c.Check1Arg(); err != nil { return nil, err } @@ -210,20 +220,57 @@ func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } + strms := &streams{} var terminalOut bool if len(c.Etc()) != 0 { tout := c.Etc()[0] - termOut, ok := tout.TryBool() - terminalOut = termOut + + var ok bool + terminalOut, ok = tout.TryBool() if !ok { - return nil, errors.New("bad argument to run (expected boolean, got " + tout.TypeName() + ")") + luastreams, ok := tout.TryTable() + if !ok { + return nil, errors.New("bad argument to run (expected boolean or table, got " + tout.TypeName() + ")") + } + + handleStream(luastreams.Get(rt.StringValue("out")), strms, false) + handleStream(luastreams.Get(rt.StringValue("err")), strms, true) + + stdinstrm := luastreams.Get(rt.StringValue("input")) + if !stdinstrm.IsNil() { + ud, ok := stdinstrm.TryUserData() + if !ok { + return nil, errors.New("bad type as run stdin stream (expected userdata as either sink or file, got " + stdinstrm.TypeName() + ")") + } + + val := ud.Value() + var varstrm io.Reader + if f, ok := val.(*iolib.File); ok { + varstrm = f.Handle() + } + + if f, ok := val.(*sink); ok { + varstrm = f.reader + } + + if varstrm == nil { + return nil, errors.New("bad type as run stdin stream (expected userdata as either sink or file)") + } + + strms.stdin = varstrm + } + } else { + if !terminalOut { + strms = &streams{ + stdout: new(bytes.Buffer), + stderr: new(bytes.Buffer), + } + } } - } else { - terminalOut = true } var exitcode uint8 - stdout, stderr, err := execCommand(cmd, terminalOut) + stdout, stderr, err := execCommand(cmd, strms) if code, ok := interp.IsExitStatus(err); ok { exitcode = code @@ -231,18 +278,20 @@ func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { exitcode = 1 } - stdoutStr := "" - stderrStr := "" - if !terminalOut { - stdoutStr = stdout.(*bytes.Buffer).String() - stderrStr = stderr.(*bytes.Buffer).String() + var stdoutStr, stderrStr string + if stdoutBuf, ok := stdout.(*bytes.Buffer); ok { + stdoutStr = stdoutBuf.String() + } + if stderrBuf, ok := stderr.(*bytes.Buffer); ok { + stderrStr = stderrBuf.String() } return c.PushingNext(t.Runtime, rt.IntValue(int64(exitcode)), rt.StringValue(stdoutStr), rt.StringValue(stderrStr)), nil } -// cwd() -// Returns the current directory of the shell +// cwd() -> string +// Returns the current directory of the shell. +// #returns string func hlcwd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { cwd, _ := os.Getwd() @@ -250,21 +299,28 @@ func hlcwd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } -// read(prompt) -> input? +// read(prompt) -> input (string) // Read input from the user, using Hilbish's line editor/input reader. // This is a separate instance from the one Hilbish actually uses. -// Returns `input`, will be nil if ctrl + d is pressed, or an error occurs (which shouldn't happen) -// --- @param prompt string +// Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs. +// #param prompt? string Text to print before input, can be empty. +// #returns string|nil func hlread(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - if err := c.Check1Arg(); err != nil { - return nil, err + luaprompt := c.Arg(0) + if typ := luaprompt.Type(); typ != rt.StringType && typ != rt.NilType { + return nil, errors.New("expected #1 to be a string") } - luaprompt, err := c.StringArg(0) - if err != nil { - return nil, err + prompt, ok := luaprompt.TryString() + if !ok { + // if we are here and `luaprompt` is not a string, it's nil + // substitute with an empty string + prompt = "" } - lualr := newLineReader("", true) - lualr.SetPrompt(luaprompt) + + lualr := &lineReader{ + rl: readline.NewInstance(), + } + lualr.SetPrompt(prompt) input, err := lualr.Read() if err != nil { @@ -275,15 +331,22 @@ func hlread(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } /* -prompt(str, typ?) -Changes the shell prompt to `str` +prompt(str, typ) +Changes the shell prompt to the provided string. There are a few verbs that can be used in the prompt text. These will be formatted and replaced with the appropriate values. `%d` - Current working directory `%u` - Name of current user `%h` - Hostname of device ---- @param str string ---- @param typ string Type of prompt, being left or right. Left by default. +#param str string +#param typ? string Type of prompt, being left or right. Left by default. +#example +-- the default hilbish prompt without color +hilbish.prompt '%u %d ∆' +-- or something of old: +hilbish.prompt '%u@%h :%d $' +-- prompt: user@hostname: ~/directory $ +#example */ func hlprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { err := c.Check1Arg() @@ -317,8 +380,28 @@ func hlprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } // multiprompt(str) -// Changes the continued line prompt to `str` -// --- @param str string +// Changes the text prompt when Hilbish asks for more input. +// This will show up when text is incomplete, like a missing quote +// #param str string +/* +#example +--[[ +imagine this is your text input: +user ~ ∆ echo "hey + +but there's a missing quote! hilbish will now prompt you so the terminal +will look like: +user ~ ∆ echo "hey +--> ...!" + +so then you get +user ~ ∆ echo "hey +--> ...!" +hey ...! +]]-- +hilbish.multiprompt '-->' +#example +*/ func hlmultiprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -333,9 +416,19 @@ func hlmultiprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } // alias(cmd, orig) -// Sets an alias of `cmd` to `orig` -// --- @param cmd string -// --- @param orig string +// Sets an alias, with a name of `cmd` to another command. +// #param cmd string Name of the alias +// #param orig string Command that will be aliased +/* +#example +-- With this, "ga file" will turn into "git add file" +hilbish.alias('ga', 'git add') + +-- Numbered substitutions are supported here! +hilbish.alias('dircount', 'ls %1 | wc -l') +-- "dircount ~" would count how many files are in ~ (home directory). +#example +*/ func hlalias(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.CheckNArgs(2); err != nil { return nil, err @@ -355,8 +448,20 @@ func hlalias(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } // appendPath(dir) -// Appends `dir` to $PATH -// --- @param dir string|table +// Appends the provided dir to the command path (`$PATH`) +// #param dir string|table Directory (or directories) to append to path +/* +#example +hilbish.appendPath '~/go/bin' +-- Will add ~/go/bin to the command path. + +-- Or do multiple: +hilbish.appendPath { + '~/go/bin', + '~/.local/bin' +} +#example +*/ func hlappendPath(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -390,8 +495,9 @@ func appendPath(dir string) { } // exec(cmd) -// Replaces running hilbish with `cmd` -// --- @param cmd string +// Replaces the currently running Hilbish instance with the supplied command. +// This can be used to do an in-place restart. +// #param cmd string func hlexec(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -425,8 +531,11 @@ func hlexec(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } // goro(fn) -// Puts `fn` in a goroutine -// --- @param fn function +// Puts `fn` in a Goroutine. +// This can be used to run any function in another thread at the same time as other Lua code. +// **NOTE: THIS FUNCTION MAY CRASH HILBISH IF OUTSIDE VARIABLES ARE ACCESSED.** +// **This is a limitation of the Lua runtime.** +// #param fn function func hlgoro(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -438,6 +547,12 @@ func hlgoro(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // call fn go func() { + defer func() { + if r := recover(); r != nil { + // do something here? + } + }() + _, err := rt.Call1(l.MainThread(), rt.FunctionValue(fn), c.Etc()...) if err != nil { fmt.Fprintln(os.Stderr, "Error in goro function:\n\n", err) @@ -447,12 +562,12 @@ func hlgoro(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } -// timeout(cb, time) -// Runs the `cb` function after `time` in milliseconds -// Returns a `timer` object (see `doc timers`). -// --- @param cb function -// --- @param time number -// --- @return table +// timeout(cb, time) -> @Timer +// Executed the `cb` function after a period of `time`. +// This creates a Timer that starts ticking immediately. +// #param cb function +// #param time number Time to run in milliseconds. +// #returns Timer func hltimeout(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.CheckNArgs(2); err != nil { return nil, err @@ -473,12 +588,12 @@ func hltimeout(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext1(t.Runtime, rt.UserDataValue(timer.ud)), nil } -// interval(cb, time) -// Runs the `cb` function every `time` milliseconds. -// Returns a `timer` object (see `doc timers`). -// --- @param cb function -// --- @param time number -// --- @return table +// interval(cb, time) -> @Timer +// Runs the `cb` function every specified amount of `time`. +// This creates a timer that ticking immediately. +// #param cb function +// #param time number Time in milliseconds. +// #return Timer func hlinterval(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.CheckNArgs(2); err != nil { return nil, err @@ -500,13 +615,40 @@ func hlinterval(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } // complete(scope, cb) -// Registers a completion handler for `scope`. -// A `scope` is currently only expected to be `command.`, +// Registers a completion handler for the specified scope. +// A `scope` is expected to be `command.`, // replacing with the name of the command (for example `command.git`). -// `cb` must be a function that returns a table of "completion groups." -// Check `doc completions` for more information. -// --- @param scope string -// --- @param cb function +// The documentation for completions, under Features/Completions or `doc completions` +// provides more details. +// #param scope string +// #param cb function +/* +#example +-- This is a very simple example. Read the full doc for completions for details. +hilbish.complete('command.sudo', function(query, ctx, fields) + if #fields == 0 then + -- complete for commands + local comps, pfx = hilbish.completion.bins(query, ctx, fields) + local compGroup = { + items = comps, -- our list of items to complete + type = 'grid' -- what our completions will look like. + } + + return {compGroup}, pfx + end + + -- otherwise just be boring and return files + + local comps, pfx = hilbish.completion.files(query, ctx, fields) + local compGroup = { + items = comps, + type = 'grid' + } + + return {compGroup}, pfx +end) +#example +*/ func hlcomplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { scope, cb, err := util.HandleStrCallback(t, c) if err != nil { @@ -518,8 +660,8 @@ func hlcomplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } // prependPath(dir) -// Prepends `dir` to $PATH -// --- @param dir string +// Prepends `dir` to $PATH. +// #param dir string func hlprependPath(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -539,9 +681,11 @@ func hlprependPath(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } -// which(name) -// Checks if `name` is a valid command -// --- @param binName string +// which(name) -> string +// Checks if `name` is a valid command. +// Will return the path of the binary, or a basename if it's a commander. +// #param name string +// #returns string func hlwhich(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -551,10 +695,13 @@ func hlwhich(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } - cmd := aliases.Resolve(name) + // itll return either the original command or what was passed + // if name isnt empty its not an issue + alias := aliases.Resolve(name) + cmd := strings.Split(alias, " ")[0] // check for commander - if commands[cmd] != nil { + if cmds.Commands[cmd] != nil { // they dont resolve to a path, so just send the cmd return c.PushingNext1(t.Runtime, rt.StringValue(cmd)), nil } @@ -568,8 +715,10 @@ func hlwhich(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } // inputMode(mode) -// Sets the input mode for Hilbish's line reader. Accepts either emacs or vim -// --- @param mode string +// Sets the input mode for Hilbish's line reader. +// `emacs` is the default. Setting it to `vim` changes behavior of input to be +// Vim-like with modes and Vim keybinds. +// #param mode string Can be set to either `emacs` or `vim` func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -594,12 +743,14 @@ func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } // runnerMode(mode) -// Sets the execution/runner mode for interactive Hilbish. This determines whether -// Hilbish wll try to run input as Lua and/or sh or only do one of either. +// Sets the execution/runner mode for interactive Hilbish. +// This determines whether Hilbish wll try to run input as Lua +// and/or sh or only do one of either. // Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua), // sh, and lua. It also accepts a function, to which if it is passed one // will call it to execute user input instead. -// --- @param mode string|function +// Read [about runner mode](../features/runner-mode) for more information. +// #param mode string|function func hlrunnerMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -625,18 +776,42 @@ func hlrunnerMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // line and cursor position. It is expected to return a string which is used // as the text for the hint. This is by default a shim. To set hints, // override this function with your custom handler. -// --- @param line string -// --- @param pos int +// #param line string +// #param pos number Position of cursor in line. Usually equals string.len(line) +/* +#example +-- this will display "hi" after the cursor in a dimmed color. +function hilbish.hinter(line, pos) + return 'hi' +end +#example +*/ func hlhinter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } // highlighter(line) -// Line highlighter handler. This is mainly for syntax highlighting, but in -// reality could set the input of the prompt to *display* anything. The -// callback is passed the current line and is expected to return a line that -// will be used as the input display. -// --- @param line string +// Line highlighter handler. +// This is mainly for syntax highlighting, but in reality could set the input +// of the prompt to *display* anything. The callback is passed the current line +// and is expected to return a line that will be used as the input display. +// Note that to set a highlighter, one has to override this function. +// #example +// --This code will highlight all double quoted strings in green. +// function hilbish.highlighter(line) +// return line:gsub('"%w+"', function(c) return lunacolors.green(c) end) +// end +// #example +// #param line string func hlhighlighter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - return c.Next(), nil + if err := c.Check1Arg(); err != nil { + return nil, err + } + + line, err := c.StringArg(0) + if err != nil { + return nil, err + } + + return c.PushingNext1(t.Runtime, rt.StringValue(line)), nil } diff --git a/assets/hilbish-flower.png b/assets/hilbish-flower.png index b4fb0f7..866e57e 100644 Binary files a/assets/hilbish-flower.png and b/assets/hilbish-flower.png differ diff --git a/assets/hilbish-logo-and-text.png b/assets/hilbish-logo-and-text.png new file mode 100644 index 0000000..325034c Binary files /dev/null and b/assets/hilbish-logo-and-text.png differ diff --git a/assets/hilbish-text.png b/assets/hilbish-text.png deleted file mode 100644 index 16412c4..0000000 Binary files a/assets/hilbish-text.png and /dev/null differ diff --git a/cmd/docgen/docgen.go b/cmd/docgen/docgen.go index 39a2a76..bf8fd1b 100644 --- a/cmd/docgen/docgen.go +++ b/cmd/docgen/docgen.go @@ -7,28 +7,300 @@ import ( "go/doc" "go/parser" "go/token" + "regexp" "strings" "os" + "sync" + + md "github.com/atsushinee/go-markdown-generator/doc" ) -type EmmyPiece struct { - FuncName string - Docs []string +var header = `--- +title: %s %s +description: %s +layout: doc +menu: + docs: + parent: "API" +--- + +` + +type emmyPiece struct { + DocPiece *docPiece + Annotations []string Params []string // we only need to know param name to put in function + FuncName string } -type DocPiece struct { + +type module struct { + Types []docPiece + Docs []docPiece + Fields []docPiece + Properties []docPiece + ShortDescription string + Description string + ParentModule string + HasInterfaces bool + HasTypes bool +} + +type param struct{ + Name string + Type string + Doc []string +} + +type docPiece struct { Doc []string FuncSig string FuncName string + Interfacing string + ParentModule string + GoFuncName string + IsInterface bool + IsMember bool + IsType bool + Fields []docPiece + Properties []docPiece + Params []param + Tags map[string][]tag +} + +type tag struct { + id string + fields []string + startIdx int +} + +var docs = make(map[string]module) +var interfaceDocs = make(map[string]module) +var emmyDocs = make(map[string][]emmyPiece) +var typeTable = make(map[string][]string) // [0] = parentMod, [1] = interfaces +var prefix = map[string]string{ + "main": "hl", + "hilbish": "hl", + "fs": "f", + "commander": "c", + "bait": "b", + "terminal": "term", +} + +func getTagsAndDocs(docs string) (map[string][]tag, []string) { + pts := strings.Split(docs, "\n") + parts := []string{} + tags := make(map[string][]tag) + + for idx, part := range pts { + if strings.HasPrefix(part, "#") { + tagParts := strings.Split(strings.TrimPrefix(part, "#"), " ") + if tags[tagParts[0]] == nil { + var id string + if len(tagParts) > 1 { + id = tagParts[1] + } + tags[tagParts[0]] = []tag{ + {id: id, startIdx: idx}, + } + if len(tagParts) >= 2 { + tags[tagParts[0]][0].fields = tagParts[2:] + } + } else { + if tagParts[0] == "example" { + exampleIdx := tags["example"][0].startIdx + exampleCode := pts[exampleIdx+1:idx] + + tags["example"][0].fields = exampleCode + parts = strings.Split(strings.Replace(strings.Join(parts, "\n"), strings.TrimPrefix(strings.Join(exampleCode, "\n"), "#example\n"), "", -1), "\n") + continue + } + + fleds := []string{} + if len(tagParts) >= 2 { + fleds = tagParts[2:] + } + tags[tagParts[0]] = append(tags[tagParts[0]], tag{ + id: tagParts[1], + fields: fleds, + }) + } + } else { + parts = append(parts, part) + } + } + + return tags, parts +} + +func docPieceTag(tagName string, tags map[string][]tag) []docPiece { + dps := []docPiece{} + for _, tag := range tags[tagName] { + dps = append(dps, docPiece{ + FuncName: tag.id, + Doc: tag.fields, + }) + } + + return dps +} + +func setupDocType(mod string, typ *doc.Type) *docPiece { + docs := strings.TrimSpace(typ.Doc) + tags, doc := getTagsAndDocs(docs) + + if tags["type"] == nil { + return nil + } + inInterface := tags["interface"] != nil + + var interfaces string + typeName := strings.ToUpper(string(typ.Name[0])) + typ.Name[1:] + typeDoc := []string{} + + if inInterface { + interfaces = tags["interface"][0].id + } + + fields := docPieceTag("field", tags) + properties := docPieceTag("property", tags) + + for _, d := range doc { + if strings.HasPrefix(d, "---") { + // TODO: document types in lua + /* + emmyLine := strings.TrimSpace(strings.TrimPrefix(d, "---")) + emmyLinePieces := strings.Split(emmyLine, " ") + emmyType := emmyLinePieces[0] + if emmyType == "@param" { + em.Params = append(em.Params, emmyLinePieces[1]) + } + if emmyType == "@vararg" { + em.Params = append(em.Params, "...") // add vararg + } + em.Annotations = append(em.Annotations, d) + */ + } else { + typeDoc = append(typeDoc, d) + } + } + + var isMember bool + if tags["member"] != nil { + isMember = true + } + parentMod := mod + dps := &docPiece{ + Doc: typeDoc, + FuncName: typeName, + Interfacing: interfaces, + IsInterface: inInterface, + IsMember: isMember, + IsType: true, + ParentModule: parentMod, + Fields: fields, + Properties: properties, + Tags: tags, + } + + typeTable[strings.ToLower(typeName)] = []string{parentMod, interfaces} + + return dps +} + +func setupDoc(mod string, fun *doc.Func) *docPiece { + docs := strings.TrimSpace(fun.Doc) + tags, parts := getTagsAndDocs(docs) + + // i couldnt fit this into the condition below for some reason so here's a goto! + if tags["member"] != nil { + goto start + } + + if (!strings.HasPrefix(fun.Name, prefix[mod]) && tags["interface"] == nil) || (strings.ToLower(fun.Name) == "loader" && tags["interface"] == nil) { + return nil + } + +start: + inInterface := tags["interface"] != nil + var interfaces string + funcsig := parts[0] + doc := parts[1:] + funcName := strings.TrimPrefix(fun.Name, prefix[mod]) + funcdoc := []string{} + + if inInterface { + interfaces = tags["interface"][0].id + funcName = interfaces + "." + strings.Split(funcsig, "(")[0] + } + em := emmyPiece{FuncName: funcName} + + fields := docPieceTag("field", tags) + properties := docPieceTag("property", tags) + var params []param + if paramsRaw := tags["param"]; paramsRaw != nil { + params = make([]param, len(paramsRaw)) + for i, p := range paramsRaw { + params[i] = param{ + Name: p.id, + Type: p.fields[0], + Doc: p.fields[1:], + } + } + } + + for _, d := range doc { + if strings.HasPrefix(d, "---") { + emmyLine := strings.TrimSpace(strings.TrimPrefix(d, "---")) + emmyLinePieces := strings.Split(emmyLine, " ") + emmyType := emmyLinePieces[0] + if emmyType == "@param" { + em.Params = append(em.Params, emmyLinePieces[1]) + } + if emmyType == "@vararg" { + em.Params = append(em.Params, "...") // add vararg + } + em.Annotations = append(em.Annotations, d) + } else { + funcdoc = append(funcdoc, d) + } + } + + var isMember bool + if tags["member"] != nil { + isMember = true + } + var parentMod string + if inInterface { + parentMod = mod + } + dps := &docPiece{ + Doc: funcdoc, + FuncSig: funcsig, + FuncName: funcName, + Interfacing: interfaces, + GoFuncName: strings.ToLower(fun.Name), + IsInterface: inInterface, + IsMember: isMember, + ParentModule: parentMod, + Fields: fields, + Properties: properties, + Params: params, + Tags: tags, + } + if strings.HasSuffix(dps.GoFuncName, strings.ToLower("loader")) { + dps.Doc = parts + } + em.DocPiece = dps + + emmyDocs[mod] = append(emmyDocs[mod], em) + return dps } -// feel free to clean this up -// it works, dont really care about the code func main() { fset := token.NewFileSet() os.Mkdir("docs", 0777) + os.Mkdir("docs/api", 0777) os.Mkdir("emmyLuaDocs", 0777) - dirs := []string{"./"} filepath.Walk("golibs/", func (path string, info os.FileInfo, err error) error { @@ -51,120 +323,356 @@ func main() { } } - prefix := map[string]string{ - "hilbish": "hl", - "fs": "f", - "commander": "c", - "bait": "b", - "terminal": "term", - } - docs := make(map[string][]DocPiece) - emmyDocs := make(map[string][]EmmyPiece) - + interfaceModules := make(map[string]*module) for l, f := range pkgs { p := doc.New(f, "./", doc.AllDecls) + pieces := []docPiece{} + typePieces := []docPiece{} + mod := l + if mod == "main" { + mod = "hilbish" + } + var hasInterfaces bool for _, t := range p.Funcs { - mod := l - if strings.HasPrefix(t.Name, "hl") { mod = "hilbish" } - if !strings.HasPrefix(t.Name, prefix[mod]) || t.Name == "Loader" { continue } - parts := strings.Split(strings.TrimSpace(t.Doc), "\n") - funcsig := parts[0] - doc := parts[1:] - funcdoc := []string{} - em := EmmyPiece{FuncName: strings.TrimPrefix(t.Name, prefix[mod])} - for _, d := range doc { - if strings.HasPrefix(d, "---") { - emmyLine := strings.TrimSpace(strings.TrimPrefix(d, "---")) - emmyLinePieces := strings.Split(emmyLine, " ") - emmyType := emmyLinePieces[0] - if emmyType == "@param" { - em.Params = append(em.Params, emmyLinePieces[1]) - } - if emmyType == "@vararg" { - em.Params = append(em.Params, "...") // add vararg - } - em.Docs = append(em.Docs, d) - } else { - funcdoc = append(funcdoc, d) - } + piece := setupDoc(mod, t) + if piece == nil { + continue } - - dps := DocPiece{ - Doc: funcdoc, - FuncSig: funcsig, - FuncName: strings.TrimPrefix(t.Name, prefix[mod]), + + pieces = append(pieces, *piece) + if piece.IsInterface { + hasInterfaces = true } - - docs[mod] = append(docs[mod], dps) - emmyDocs[mod] = append(emmyDocs[mod], em) } for _, t := range p.Types { - for _, m := range t.Methods { - if !strings.HasPrefix(m.Name, prefix[l]) || m.Name == "Loader" { continue } - parts := strings.Split(strings.TrimSpace(m.Doc), "\n") - funcsig := parts[0] - doc := parts[1:] - funcdoc := []string{} - em := EmmyPiece{FuncName: strings.TrimPrefix(m.Name, prefix[l])} - for _, d := range doc { - if strings.HasPrefix(d, "---") { - emmyLine := strings.TrimSpace(strings.TrimPrefix(d, "---")) - emmyLinePieces := strings.Split(emmyLine, " ") - emmyType := emmyLinePieces[0] - if emmyType == "@param" { - em.Params = append(em.Params, emmyLinePieces[1]) - } - if emmyType == "@vararg" { - em.Params = append(em.Params, "...") // add vararg - } - em.Docs = append(em.Docs, d) - } else { - funcdoc = append(funcdoc, d) - } + typePiece := setupDocType(mod, t) + if typePiece != nil { + typePieces = append(typePieces, *typePiece) + if typePiece.IsInterface { + hasInterfaces = true } - dps := DocPiece{ - Doc: funcdoc, - FuncSig: funcsig, - FuncName: strings.TrimPrefix(m.Name, prefix[l]), + } + + for _, m := range t.Methods { + piece := setupDoc(mod, m) + if piece == nil { + continue } - docs[l] = append(docs[l], dps) - emmyDocs[l] = append(emmyDocs[l], em) + pieces = append(pieces, *piece) + if piece.IsInterface { + hasInterfaces = true + } } } + + tags, descParts := getTagsAndDocs(strings.TrimSpace(p.Doc)) + shortDesc := descParts[0] + desc := descParts[1:] + filteredPieces := []docPiece{} + filteredTypePieces := []docPiece{} + for _, piece := range pieces { + if !piece.IsInterface { + filteredPieces = append(filteredPieces, piece) + continue + } + + modname := piece.ParentModule + "." + piece.Interfacing + if interfaceModules[modname] == nil { + interfaceModules[modname] = &module{ + ParentModule: piece.ParentModule, + } + } + + if strings.HasSuffix(piece.GoFuncName, strings.ToLower("loader")) { + shortDesc := piece.Doc[0] + desc := piece.Doc[1:] + interfaceModules[modname].ShortDescription = shortDesc + interfaceModules[modname].Description = strings.Join(desc, "\n") + interfaceModules[modname].Fields = piece.Fields + interfaceModules[modname].Properties = piece.Properties + continue + } + + interfaceModules[modname].Docs = append(interfaceModules[modname].Docs, piece) + } + + for _, piece := range typePieces { + if !piece.IsInterface { + filteredTypePieces = append(filteredTypePieces, piece) + continue + } + + modname := piece.ParentModule + "." + piece.Interfacing + if interfaceModules[modname] == nil { + interfaceModules[modname] = &module{ + ParentModule: piece.ParentModule, + } + } + + interfaceModules[modname].Types = append(interfaceModules[modname].Types, piece) + } + + docs[mod] = module{ + Types: filteredTypePieces, + Docs: filteredPieces, + ShortDescription: shortDesc, + Description: strings.Join(desc, "\n"), + HasInterfaces: hasInterfaces, + Properties: docPieceTag("property", tags), + Fields: docPieceTag("field", tags), + } } + for key, mod := range interfaceModules { + docs[key] = *mod + } + + var wg sync.WaitGroup + wg.Add(len(docs) * 2) + for mod, v := range docs { - if mod == "main" { continue } - f, _ := os.Create("docs/" + mod + ".txt") - for _, dps := range v { - f.WriteString(dps.FuncSig + " > ") - for _, doc := range dps.Doc { - if !strings.HasPrefix(doc, "---") { - f.WriteString(doc + "\n") + docPath := "docs/api/" + mod + ".md" + if v.HasInterfaces { + os.Mkdir("docs/api/" + mod, 0777) + os.Remove(docPath) // remove old doc path if it exists + docPath = "docs/api/" + mod + "/_index.md" + } + if v.ParentModule != "" { + docPath = "docs/api/" + v.ParentModule + "/" + mod + ".md" + } + + go func(modname, docPath string, modu module) { + defer wg.Done() + modOrIface := "Module" + if modu.ParentModule != "" { + modOrIface = "Module" + } + lastHeader := "" + + f, _ := os.Create(docPath) + f.WriteString(fmt.Sprintf(header, modOrIface, modname, modu.ShortDescription)) + typeTag, _ := regexp.Compile(`\B@\w+`) + modDescription := typeTag.ReplaceAllStringFunc(strings.Replace(strings.Replace(modu.Description, "<", `\<`, -1), "{{\\<", "{{<", -1), func(typ string) string { + typName := typ[1:] + typLookup := typeTable[strings.ToLower(typName)] + ifaces := typLookup[0] + "." + typLookup[1] + "/" + if typLookup[1] == "" { + ifaces = "" + } + linkedTyp := fmt.Sprintf("/Hilbish/docs/api/%s/%s#%s", typLookup[0], ifaces, strings.ToLower(typName)) + return fmt.Sprintf(`%s`, linkedTyp, typName) + }) + f.WriteString(fmt.Sprintf("## Introduction\n%s\n\n", modDescription)) + if len(modu.Docs) != 0 { + funcCount := 0 + for _, dps := range modu.Docs { + if dps.IsMember { + continue + } + funcCount++ + } + + f.WriteString("## Functions\n") + lastHeader = "functions" + + mdTable := md.NewTable(funcCount, 2) + mdTable.SetTitle(0, "") + mdTable.SetTitle(1, "") + + diff := 0 + for i, dps := range modu.Docs { + if dps.IsMember { + diff++ + continue + } + + mdTable.SetContent(i - diff, 0, fmt.Sprintf(`%s`, dps.FuncName, dps.FuncSig)) + if len(dps.Doc) == 0 { + fmt.Printf("WARNING! Function %s on module %s has no documentation!\n", dps.FuncName, modname) + } else { + mdTable.SetContent(i - diff, 1, dps.Doc[0]) + } + } + f.WriteString(mdTable.String()) + f.WriteString("\n") + } + + if len(modu.Fields) != 0 { + f.WriteString("## Static module fields\n") + + mdTable := md.NewTable(len(modu.Fields), 2) + mdTable.SetTitle(0, "") + mdTable.SetTitle(1, "") + + + for i, dps := range modu.Fields { + mdTable.SetContent(i, 0, dps.FuncName) + mdTable.SetContent(i, 1, strings.Join(dps.Doc, " ")) + } + f.WriteString(mdTable.String()) + f.WriteString("\n") + } + if len(modu.Properties) != 0 { + f.WriteString("## Object properties\n") + + mdTable := md.NewTable(len(modu.Fields), 2) + mdTable.SetTitle(0, "") + mdTable.SetTitle(1, "") + + + for i, dps := range modu.Properties { + mdTable.SetContent(i, 0, dps.FuncName) + mdTable.SetContent(i, 1, strings.Join(dps.Doc, " ")) + } + f.WriteString(mdTable.String()) + f.WriteString("\n") + } + + if len(modu.Docs) != 0 { + if lastHeader != "functions" { + f.WriteString("## Functions\n") + } + for _, dps := range modu.Docs { + if dps.IsMember { + continue + } + f.WriteString(fmt.Sprintf("
\n
", dps.FuncName)) + htmlSig := typeTag.ReplaceAllStringFunc(strings.Replace(modname + "." + dps.FuncSig, "<", `\<`, -1), func(typ string) string { + typName := typ[1:] + typLookup := typeTable[strings.ToLower(typName)] + ifaces := typLookup[0] + "." + typLookup[1] + "/" + if typLookup[1] == "" { + ifaces = "" + } + linkedTyp := fmt.Sprintf("/Hilbish/docs/api/%s/%s#%s", typLookup[0], ifaces, strings.ToLower(typName)) + return fmt.Sprintf(`%s`, linkedTyp, typName) + }) + f.WriteString(fmt.Sprintf(` +

+%s + + + +

+ +`, htmlSig, dps.FuncName)) + for _, doc := range dps.Doc { + if !strings.HasPrefix(doc, "---") && doc != "" { + f.WriteString(doc + " \n") + } + } + f.WriteString("\n#### Parameters\n") + if len(dps.Params) == 0 { + f.WriteString("This function has no parameters. \n") + } + for _, p := range dps.Params { + isVariadic := false + typ := p.Type + if strings.HasPrefix(p.Type, "...") { + isVariadic = true + typ = p.Type[3:] + } + + f.WriteString(fmt.Sprintf("`%s` **`%s`**", typ, p.Name)) + if isVariadic { + f.WriteString(" (This type is variadic. You can pass an infinite amount of parameters with this type.)") + } + f.WriteString(" \n") + f.WriteString(strings.Join(p.Doc, " ")) + f.WriteString("\n\n") + } + if codeExample := dps.Tags["example"]; codeExample != nil { + f.WriteString("#### Example\n") + f.WriteString(fmt.Sprintf("```lua\n%s\n```\n", strings.Join(codeExample[0].fields, "\n"))) + } + f.WriteString("
") + f.WriteString("\n\n") } } - f.WriteString("\n") - } - } - - for mod, v := range emmyDocs { - if mod == "main" { continue } - f, _ := os.Create("emmyLuaDocs/" + mod + ".lua") - f.WriteString("--- @meta\n\nlocal " + mod + " = {}\n\n") - for _, em := range v { - var funcdocs []string - for _, dps := range docs[mod] { - if dps.FuncName == em.FuncName { - funcdocs = dps.Doc + + if len(modu.Types) != 0 { + f.WriteString("## Types\n") + for _, dps := range modu.Types { + f.WriteString("
\n\n") + f.WriteString(fmt.Sprintf("## %s\n", dps.FuncName)) + for _, doc := range dps.Doc { + if !strings.HasPrefix(doc, "---") { + f.WriteString(doc + "\n") + } + } + if len(dps.Properties) != 0 { + f.WriteString("## Object properties\n") + + mdTable := md.NewTable(len(dps.Properties), 2) + mdTable.SetTitle(0, "") + mdTable.SetTitle(1, "") + + for i, d := range dps.Properties { + mdTable.SetContent(i, 0, d.FuncName) + mdTable.SetContent(i, 1, strings.Join(d.Doc, " ")) + } + f.WriteString(mdTable.String()) + f.WriteString("\n") + } + f.WriteString("\n") + f.WriteString("### Methods\n") + for _, dps := range modu.Docs { + if !dps.IsMember { + continue + } + htmlSig := typeTag.ReplaceAllStringFunc(strings.Replace(dps.FuncSig, "<", `\<`, -1), func(typ string) string { + typName := regexp.MustCompile(`\w+`).FindString(typ[1:]) + typLookup := typeTable[strings.ToLower(typName)] + fmt.Printf("%+q, \n", typLookup) + linkedTyp := fmt.Sprintf("/Hilbish/docs/api/%s/%s/#%s", typLookup[0], typLookup[0] + "." + typLookup[1], strings.ToLower(typName)) + return fmt.Sprintf(`%s`, linkedTyp, typName) + }) + f.WriteString(fmt.Sprintf("#### %s\n", htmlSig)) + for _, doc := range dps.Doc { + if !strings.HasPrefix(doc, "---") { + f.WriteString(doc + "\n") + } + } + f.WriteString("\n") + } } } - f.WriteString("--- " + strings.Join(funcdocs, "\n--- ") + "\n") - if len(em.Docs) != 0 { - f.WriteString(strings.Join(em.Docs, "\n") + "\n") + }(mod, docPath, v) + + go func(md, modname string, modu module) { + defer wg.Done() + + if modu.ParentModule != "" { + return } - f.WriteString("function " + mod + "." + em.FuncName + "(" + strings.Join(em.Params, ", ") + ") end\n\n") - } - f.WriteString("return " + mod + "\n") + + ff, _ := os.Create("emmyLuaDocs/" + modname + ".lua") + ff.WriteString("--- @meta\n\nlocal " + modname + " = {}\n\n") + for _, em := range emmyDocs[modname] { + if strings.HasSuffix(em.DocPiece.GoFuncName, strings.ToLower("loader")) { + continue + } + + dps := em.DocPiece + funcdocs := dps.Doc + ff.WriteString("--- " + strings.Join(funcdocs, "\n--- ") + "\n") + if len(em.Annotations) != 0 { + ff.WriteString(strings.Join(em.Annotations, "\n") + "\n") + } + accessor := "." + if dps.IsMember { + accessor = ":" + } + signature := strings.Split(dps.FuncSig, " ->")[0] + var intrface string + if dps.IsInterface { + intrface = "." + dps.Interfacing + } + ff.WriteString("function " + modname + intrface + accessor + signature + " end\n\n") + } + ff.WriteString("return " + modname + "\n") + }(mod, mod, v) } + wg.Wait() } diff --git a/cmd/docgen/docgen.lua b/cmd/docgen/docgen.lua new file mode 100644 index 0000000..207357a --- /dev/null +++ b/cmd/docgen/docgen.lua @@ -0,0 +1,146 @@ +local fs = require 'fs' +local emmyPattern = '^%-%-%- (.+)' +local modpattern = '^%-+ @module (%w+)' +local pieces = {} + +local files = fs.readdir 'nature' +for _, fname in ipairs(files) do + local isScript = fname:match'%.lua$' + if not isScript then goto continue end + + local f = io.open(string.format('nature/%s', fname)) + local header = f:read '*l' + local mod = header:match(modpattern) + if not mod then goto continue end + + print(fname, mod) + pieces[mod] = {} + + local docPiece = {} + local lines = {} + local lineno = 0 + for line in f:lines() do + lineno = lineno + 1 + lines[lineno] = line + + if line == header then goto continue2 end + if not line:match(emmyPattern) then + if line:match '^function' then + local pattern = (string.format('^function %s%%.', mod) .. '(%w+)') + local funcName = line:match(pattern) + if not funcName then goto continue2 end + + local dps = { + description = {}, + params = {} + } + + local offset = 1 + while true do + local prev = lines[lineno - offset] + + local docline = prev:match '^%-+ (.+)' + if docline then + local emmy = docline:match '@(%w+)' + local cut = 0 + + if emmy then cut = emmy:len() + 3 end + local emmythings = string.split(docline:sub(cut), ' ') + + if emmy then + if emmy == 'param' then + table.insert(dps.params, 1, { + name = emmythings[1], + type = emmythings[2] + }) + end + else + table.insert(dps.description, 1, docline) + end + offset = offset + 1 + else + break + end + end + + pieces[mod][funcName] = dps + end + docPiece = {} + goto continue2 + end + + table.insert(docPiece, line) + ::continue2:: + end + ::continue:: +end + +local header = [[--- +title: %s %s +description: %s +layout: doc +menu: + docs: + parent: "Nature" +--- + +]] + +for iface, dps in pairs(pieces) do + local mod = iface:match '(%w+)%.' or 'nature' + local path = string.format('docs/%s/%s.md', mod, iface) + fs.mkdir(fs.dir(path), true) + local f = io.open(path, 'w') + f:write(string.format(header, 'Module', iface, 'No description.')) + print(f) + + print(mod, path) + + for func, docs in pairs(dps) do + f:write(string.format('
\n
', func)) + local sig = string.format('%s.%s(', iface, func) + for idx, param in ipairs(docs.params) do + sig = sig .. ((param.name:gsub('%?$', ''))) + if idx ~= #docs.params then sig = sig .. ', ' end + end + sig = sig .. ')' + f:write(string.format([[ +

+%s + + + +

+ +]], sig, func)) + + f:write(table.concat(docs.description, '\n') .. '\n') + f:write '#### Parameters\n' + if #docs.params == 0 then + f:write 'This function has no parameters. \n' + end + for _, param in ipairs(docs.params) do + f:write(string.format('`%s` **`%s`**\n', param.name:gsub('%?$', ''), param.type)) + end + --[[ + local params = table.filter(docs, function(t) + return t:match '^%-%-%- @param' + end) + for i, str in ipairs(params) do + if i ~= 1 then + f:write ', ' + end + f:write(str:match '^%-%-%- @param ([%w]+) ') + end + f:write(')\n') + + for _, str in ipairs(docs) do + if not str:match '^%-%-%- @' then + f:write(str:match '^%-%-%- (.+)' .. '\n') + end + end + ]]-- + f:write('
') + f:write('\n\n') + end +end diff --git a/complete.go b/complete.go index 76d65f7..1c40b20 100644 --- a/complete.go +++ b/complete.go @@ -11,15 +11,49 @@ import ( rt "github.com/arnodel/golua/runtime" ) -func splitQuote(str string) []string { +var charEscapeMap = []string{ + "\"", "\\\"", + "'", "\\'", + "`", "\\`", + " ", "\\ ", + "(", "\\(", + ")", "\\)", + "[", "\\[", + "]", "\\]", + "$", "\\$", + "&", "\\&", + "*", "\\*", + ">", "\\>", + "<", "\\<", + "|", "\\|", +} +var charEscapeMapInvert = invert(charEscapeMap) +var escapeReplaer = strings.NewReplacer(charEscapeMap...) +var escapeInvertReplaer = strings.NewReplacer(charEscapeMapInvert...) + +func invert(m []string) []string { + newM := make([]string, len(charEscapeMap)) + for i := range m { + if (i + 1) % 2 == 0 { + newM[i] = m[i - 1] + newM[i - 1] = m[i] + } + } + + return newM +} + +func splitForFile(str string) []string { split := []string{} sb := &strings.Builder{} quoted := false - for _, r := range str { + for i, r := range str { if r == '"' { quoted = !quoted sb.WriteRune(r) + } else if r == ' ' && str[i - 1] == '\\' { + sb.WriteRune(r) } else if !quoted && r == ' ' { split = append(split, sb.String()) sb.Reset() @@ -39,12 +73,22 @@ func splitQuote(str string) []string { } func fileComplete(query, ctx string, fields []string) ([]string, string) { - q := splitQuote(ctx) + q := splitForFile(ctx) + path := "" + if len(q) != 0 { + path = q[len(q) - 1] + } - return matchPath(q[len(q) - 1]) + return matchPath(path) } func binaryComplete(query, ctx string, fields []string) ([]string, string) { + q := splitForFile(ctx) + query = "" + if len(q) != 0 { + query = q[len(q) - 1] + } + var completions []string prefixes := []string{"./", "../", "/", "~/"} @@ -54,7 +98,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) { if len(fileCompletions) != 0 { for _, f := range fileCompletions { fullPath, _ := filepath.Abs(util.ExpandHome(query + strings.TrimPrefix(f, filePref))) - if err := findExecutable(fullPath, false, true); err != nil { + if err := findExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil { continue } completions = append(completions, f) @@ -66,7 +110,6 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) { // filter out executables, but in path for _, dir := range filepath.SplitList(os.Getenv("PATH")) { - // print dir to stderr for debugging // search for an executable which matches our query string if matches, err := filepath.Glob(filepath.Join(dir, query + "*")); err == nil { // get basename from matches @@ -85,7 +128,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) { } // add lua registered commands to completions - for cmdName := range commands { + for cmdName := range cmds.Commands { if strings.HasPrefix(cmdName, query) { completions = append(completions, cmdName) } @@ -102,6 +145,7 @@ func matchPath(query string) ([]string, string) { var entries []string var baseName string + query = escapeInvertReplaer.Replace(query) path, _ := filepath.Abs(util.ExpandHome(filepath.Dir(query))) if string(query) == "" { // filepath base below would give us "." @@ -112,7 +156,16 @@ func matchPath(query string) ([]string, string) { } files, _ := os.ReadDir(path) - for _, file := range files { + for _, entry := range files { + // should we handle errors here? + file, err := entry.Info() + if err == nil && file.Mode() & os.ModeSymlink != 0 { + path, err := filepath.EvalSymlinks(filepath.Join(path, file.Name())) + if err == nil { + file, err = os.Lstat(path) + } + } + if strings.HasPrefix(file.Name(), baseName) { entry := file.Name() if file.IsDir() { @@ -124,38 +177,26 @@ func matchPath(query string) ([]string, string) { entries = append(entries, entry) } } + if !strings.HasPrefix(oldQuery, "\"") { + baseName = escapeFilename(baseName) + } return entries, baseName } func escapeFilename(fname string) string { - args := []string{ - "\"", "\\\"", - "'", "\\'", - "`", "\\`", - " ", "\\ ", - "(", "\\(", - ")", "\\)", - "[", "\\[", - "]", "\\]", - "$", "\\$", - "&", "\\&", - "*", "\\*", - ">", "\\>", - "<", "\\<", - "|", "\\|", - } - - r := strings.NewReplacer(args...) - return r.Replace(fname) + return escapeReplaer.Replace(fname) } +// #interface completion +// tab completions +// The completions interface deals with tab completions. func completionLoader(rtm *rt.Runtime) *rt.Table { exports := map[string]util.LuaExport{ - "files": {luaFileComplete, 3, false}, - "bins": {luaBinaryComplete, 3, false}, - "call": {callLuaCompleter, 4, false}, - "handler": {completionHandler, 2, false}, + "bins": {hcmpBins, 3, false}, + "call": {hcmpCall, 4, false}, + "files": {hcmpFiles, 3, false}, + "handler": {hcmpHandler, 2, false}, } mod := rt.NewTable() @@ -164,12 +205,58 @@ func completionLoader(rtm *rt.Runtime) *rt.Table { return mod } -// left as a shim, might doc in the same way as hilbish functions -func completionHandler(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - return c.Next(), nil +// #interface completion +// bins(query, ctx, fields) -> entries (table), prefix (string) +// Return binaries/executables based on the provided parameters. +// This function is meant to be used as a helper in a command completion handler. +// #param query string +// #param ctx string +// #param fields table +/* +#example +-- an extremely simple completer for sudo. +hilbish.complete('command.sudo', function(query, ctx, fields) + table.remove(fields, 1) + if #fields[1] then + -- return commands because sudo runs a command as root..! + + local entries, pfx = hilbish.completion.bins(query, ctx, fields) + return { + type = 'grid', + items = entries + }, pfx + end + + -- ... else suggest files or anything else .. +end) +#example +*/ +func hcmpBins(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + query, ctx, fds, err := getCompleteParams(t, c) + if err != nil { + return nil, err + } + + completions, pfx := binaryComplete(query, ctx, fds) + luaComps := rt.NewTable() + + for i, comp := range completions { + luaComps.Set(rt.IntValue(int64(i + 1)), rt.StringValue(comp)) + } + + return c.PushingNext(t.Runtime, rt.TableValue(luaComps), rt.StringValue(pfx)), nil } -func callLuaCompleter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +// #interface completion +// call(name, query, ctx, fields) -> completionGroups (table), prefix (string) +// Calls a completer function. This is mainly used to call a command completer, which will have a `name` +// in the form of `command.name`, example: `command.git`. +// You can check the Completions doc or `doc completions` for info on the `completionGroups` return value. +// #param name string +// #param query string +// #param ctx string +// #param fields table +func hcmpCall(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.CheckNArgs(4); err != nil { return nil, err } @@ -197,18 +284,26 @@ func callLuaCompleter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } // we must keep the holy 80 cols - completerReturn, err := rt.Call1(l.MainThread(), - rt.FunctionValue(completecb), rt.StringValue(query), - rt.StringValue(ctx), rt.TableValue(fields)) + cont := c.Next() + err = rt.Call(l.MainThread(), rt.FunctionValue(completecb), + []rt.Value{rt.StringValue(query), rt.StringValue(ctx), rt.TableValue(fields)}, + cont) if err != nil { return nil, err } - return c.PushingNext1(t.Runtime, completerReturn), nil + return cont, nil } -func luaFileComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +// #interface completion +// files(query, ctx, fields) -> entries (table), prefix (string) +// Returns file matches based on the provided parameters. +// This function is meant to be used as a helper in a command completion handler. +// #param query string +// #param ctx string +// #param fields table +func hcmpFiles(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { query, ctx, fds, err := getCompleteParams(t, c) if err != nil { return nil, err @@ -224,22 +319,32 @@ func luaFileComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext(t.Runtime, rt.TableValue(luaComps), rt.StringValue(pfx)), nil } -func luaBinaryComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - query, ctx, fds, err := getCompleteParams(t, c) - if err != nil { - return nil, err - } +// #interface completion +// handler(line, pos) +// This function contains the general completion handler for Hilbish. This function handles +// completion of everything, which includes calling other command handlers, binaries, and files. +// This function can be overriden to supply a custom handler. Note that alias resolution is required to be done in this function. +// #param line string The current Hilbish command line +// #param pos number Numerical position of the cursor +/* +#example +-- stripped down version of the default implementation +function hilbish.completion.handler(line, pos) + local query = fields[#fields] - completions, pfx := binaryComplete(query, ctx, fds) - luaComps := rt.NewTable() - - for i, comp := range completions { - luaComps.Set(rt.IntValue(int64(i + 1)), rt.StringValue(comp)) - } - - return c.PushingNext(t.Runtime, rt.TableValue(luaComps), rt.StringValue(pfx)), nil + if #fields == 1 then + -- call bins handler here + else + -- call command completer or files completer here + end +end +#example +*/ +func hcmpHandler(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + return c.Next(), nil } + func getCompleteParams(t *rt.Thread, c *rt.GoCont) (string, string, []string, error) { if err := c.CheckNArgs(3); err != nil { return "", "", []string{}, err diff --git a/docs/_index.md b/docs/_index.md new file mode 100644 index 0000000..32dbf84 --- /dev/null +++ b/docs/_index.md @@ -0,0 +1,13 @@ +--- +title: Introduction +layout: doc +weight: -1 +menu: docs +--- + +Hilbish is a hyper-extensible shell mainly intended for interactive use. +To enhance the interactive experience, Hilbish comes with a wide range +of features and sane defaults, including a nice looking prompt, +advanced completion menus and history search. + +Here documents some of the features of Hilbish and the Lua API. diff --git a/docs/api/_index.md b/docs/api/_index.md new file mode 100644 index 0000000..f34539e --- /dev/null +++ b/docs/api/_index.md @@ -0,0 +1,9 @@ +--- +title: API +layout: doc +weight: -100 +menu: docs +--- + +Welcome to the API documentation for Hilbish. This documents Lua functions +provided by Hilbish. diff --git a/docs/api/bait.md b/docs/api/bait.md new file mode 100644 index 0000000..60b1056 --- /dev/null +++ b/docs/api/bait.md @@ -0,0 +1,167 @@ +--- +title: Module bait +description: the event emitter +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction + +Bait is the event emitter for Hilbish. Much like Node.js and +its `events` system, many actions in Hilbish emit events. +Unlike Node.js, Hilbish events are global. So make sure to +pick a unique name! + +Usage of the Bait module consists of userstanding +event-driven architecture, but it's pretty simple: +If you want to act on a certain event, you can `catch` it. +You can act on events via callback functions. + +Examples of this are in the Hilbish default config! +Consider this part of it: +```lua +bait.catch('command.exit', function(code) + running = false + doPrompt(code ~= 0) + doNotifyPrompt() +end) +``` + +What this does is, whenever the `command.exit` event is thrown, +this function will set the user prompt. + +## Functions +||| +|----|----| +|catch(name, cb)|Catches an event. This function can be used to act on events.| +|catchOnce(name, cb)|Catches an event, but only once. This will remove the hook immediately after it runs for the first time.| +|hooks(name) -> table|Returns a table of functions that are hooked on an event with the corresponding `name`.| +|release(name, catcher)|Removes the `catcher` for the event with `name`.| +|throw(name, ...args)|Throws a hook with `name` with the provided `args`.| + +
+
+

+bait.catch(name, cb) + + + +

+ +Catches an event. This function can be used to act on events. + +#### Parameters +`string` **`name`** +The name of the hook. + +`function` **`cb`** +The function that will be called when the hook is thrown. + +#### Example +```lua +bait.catch('hilbish.exit', function() + print 'Goodbye Hilbish!' +end) +``` +
+ +
+
+

+bait.catchOnce(name, cb) + + + +

+ +Catches an event, but only once. This will remove the hook immediately after it runs for the first time. + +#### Parameters +`string` **`name`** +The name of the event + +`function` **`cb`** +The function that will be called when the event is thrown. + +
+ +
+
+

+bait.hooks(name) -> table + + + +

+ +Returns a table of functions that are hooked on an event with the corresponding `name`. + +#### Parameters +`string` **`name`** +The name of the hook + +
+ +
+
+

+bait.release(name, catcher) + + + +

+ +Removes the `catcher` for the event with `name`. +For this to work, `catcher` has to be the same function used to catch +an event, like one saved to a variable. + +#### Parameters +`string` **`name`** +Name of the event the hook is on + +`function` **`catcher`** +Hook function to remove + +#### Example +```lua +local hookCallback = function() print 'hi' end + +bait.catch('event', hookCallback) + +-- a little while later.... +bait.release('event', hookCallback) +-- and now hookCallback will no longer be ran for the event. +``` +
+ +
+
+

+bait.throw(name, ...args) + + + +

+ +Throws a hook with `name` with the provided `args`. + +#### Parameters +`string` **`name`** +The name of the hook. + +`any` **`args`** (This type is variadic. You can pass an infinite amount of parameters with this type.) +The arguments to pass to the hook. + +#### Example +```lua +bait.throw('greeting', 'world') + +-- This can then be listened to via +bait.catch('gretting', function(greetTo) + print('Hello ' .. greetTo) +end) +``` +
+ diff --git a/docs/api/commander.md b/docs/api/commander.md new file mode 100644 index 0000000..b910706 --- /dev/null +++ b/docs/api/commander.md @@ -0,0 +1,113 @@ +--- +title: Module commander +description: library for custom commands +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction + +Commander is the library which handles Hilbish commands. This makes +the user able to add Lua-written commands to their shell without making +a separate script in a bin folder. Instead, you may simply use the Commander +library in your Hilbish config. + +```lua +local commander = require 'commander' + +commander.register('hello', function(args, sinks) + sinks.out:writeln 'Hello world!' +end) +``` + +In this example, a command with the name of `hello` is created +that will print `Hello world!` to output. One question you may +have is: What is the `sinks` parameter? + +The `sinks` parameter is a table with 3 keys: `input`, `out`, and `err`. +There is an `in` alias to `input`, but it requires using the string accessor syntax (`sinks['in']`) +as `in` is also a Lua keyword, so `input` is preferred for use. +All of them are a Sink. +In the future, `sinks.in` will be removed. + +- `in` is the standard input. +You may use the read functions on this sink to get input from the user. +- `out` is standard output. +This is usually where command output should go. +- `err` is standard error. +This sink is for writing errors, as the name would suggest. + +## Functions +||| +|----|----| +|deregister(name)|Removes the named command. Note that this will only remove Commander-registered commands.| +|register(name, cb)|Adds a new command with the given `name`. When Hilbish has to run a command with a name,| +|registry() -> table|Returns all registered commanders. Returns a list of tables with the following keys:| + +
+
+

+commander.deregister(name) + + + +

+ +Removes the named command. Note that this will only remove Commander-registered commands. + +#### Parameters +`string` **`name`** +Name of the command to remove. + +
+ +
+
+

+commander.register(name, cb) + + + +

+ +Adds a new command with the given `name`. When Hilbish has to run a command with a name, +it will run the function providing the arguments and sinks. + +#### Parameters +`string` **`name`** +Name of the command + +`function` **`cb`** +Callback to handle command invocation + +#### Example +```lua +-- When you run the command `hello` in the shell, it will print `Hello world`. +-- If you run it with, for example, `hello Hilbish`, it will print 'Hello Hilbish' +commander.register('hello', function(args, sinks) + local name = 'world' + if #args > 0 then name = args[1] end + + sinks.out:writeln('Hello ' .. name) +end) +``` +
+ +
+
+

+commander.registry() -> table + + + +

+ +Returns all registered commanders. Returns a list of tables with the following keys: +- `exec`: The function used to run the commander. Commanders require args and sinks to be passed. + +#### Parameters +This function has no parameters. +
+ diff --git a/docs/api/fs.md b/docs/api/fs.md new file mode 100644 index 0000000..7b733ef --- /dev/null +++ b/docs/api/fs.md @@ -0,0 +1,257 @@ +--- +title: Module fs +description: filesystem interaction and functionality library +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction + +The fs module provides filesystem functions to Hilbish. While Lua's standard +library has some I/O functions, they're missing a lot of the basics. The `fs` +library offers more functions and will work on any operating system Hilbish does. + +## Functions +||| +|----|----| +|abs(path) -> string|Returns an absolute version of the `path`.| +|basename(path) -> string|Returns the "basename," or the last part of the provided `path`. If path is empty,| +|cd(dir)|Changes Hilbish's directory to `dir`.| +|dir(path) -> string|Returns the directory part of `path`. If a file path like| +|glob(pattern) -> matches (table)|Match all files based on the provided `pattern`.| +|join(...path) -> string|Takes any list of paths and joins them based on the operating system's path separator.| +|mkdir(name, recursive)|Creates a new directory with the provided `name`.| +|fpipe() -> File, File|Returns a pair of connected files, also known as a pipe.| +|readdir(path) -> table[string]|Returns a list of all files and directories in the provided path.| +|stat(path) -> {}|Returns the information about a given `path`.| + +## Static module fields +||| +|----|----| +|pathSep|The operating system's path separator.| + +
+
+

+fs.abs(path) -> string + + + +

+ +Returns an absolute version of the `path`. +This can be used to resolve short paths like `..` to `/home/user`. + +#### Parameters +`string` **`path`** + + +
+ +
+
+

+fs.basename(path) -> string + + + +

+ +Returns the "basename," or the last part of the provided `path`. If path is empty, +`.` will be returned. + +#### Parameters +`string` **`path`** +Path to get the base name of. + +
+ +
+
+

+fs.cd(dir) + + + +

+ +Changes Hilbish's directory to `dir`. + +#### Parameters +`string` **`dir`** +Path to change directory to. + +
+ +
+
+

+fs.dir(path) -> string + + + +

+ +Returns the directory part of `path`. If a file path like +`~/Documents/doc.txt` then this function will return `~/Documents`. + +#### Parameters +`string` **`path`** +Path to get the directory for. + +
+ +
+
+

+fs.glob(pattern) -> matches (table) + + + +

+ +Match all files based on the provided `pattern`. +For the syntax' refer to Go's filepath.Match function: https://pkg.go.dev/path/filepath#Match + +#### Parameters +`string` **`pattern`** +Pattern to compare files with. + +#### Example +```lua +--[[ + Within a folder that contains the following files: + a.txt + init.lua + code.lua + doc.pdf +]]-- +local matches = fs.glob './*.lua' +print(matches) +-- -> {'init.lua', 'code.lua'} +``` +
+ +
+
+

+fs.join(...path) -> string + + + +

+ +Takes any list of paths and joins them based on the operating system's path separator. + +#### Parameters +`string` **`path`** (This type is variadic. You can pass an infinite amount of parameters with this type.) +Paths to join together + +#### Example +```lua +-- This prints the directory for Hilbish's config! +print(fs.join(hilbish.userDir.config, 'hilbish')) +-- -> '/home/user/.config/hilbish' on Linux +``` +
+ +
+
+

+fs.mkdir(name, recursive) + + + +

+ +Creates a new directory with the provided `name`. +With `recursive`, mkdir will create parent directories. + +#### Parameters +`string` **`name`** +Name of the directory + +`boolean` **`recursive`** +Whether to create parent directories for the provided name + +#### Example +```lua +-- This will create the directory foo, then create the directory bar in the +-- foo directory. If recursive is false in this case, it will fail. +fs.mkdir('./foo/bar', true) +``` +
+ +
+
+

+fs.fpipe() -> File, File + + + +

+ +Returns a pair of connected files, also known as a pipe. +The type returned is a Lua file, same as returned from `io` functions. + +#### Parameters +This function has no parameters. +
+ +
+
+

+fs.readdir(path) -> table[string] + + + +

+ +Returns a list of all files and directories in the provided path. + +#### Parameters +`string` **`dir`** + + +
+ +
+
+

+fs.stat(path) -> {} + + + +

+ +Returns the information about a given `path`. +The returned table contains the following values: +name (string) - Name of the path +size (number) - Size of the path in bytes +mode (string) - Unix permission mode in an octal format string (with leading 0) +isDir (boolean) - If the path is a directory + +#### Parameters +`string` **`path`** + + +#### Example +```lua +local inspect = require 'inspect' + +local stat = fs.stat '~' +print(inspect(stat)) +--[[ +Would print the following: +{ + isDir = true, + mode = "0755", + name = "username", + size = 12288 +} +]]-- +``` +
+ diff --git a/docs/api/hilbish/_index.md b/docs/api/hilbish/_index.md new file mode 100644 index 0000000..5c7a0f0 --- /dev/null +++ b/docs/api/hilbish/_index.md @@ -0,0 +1,544 @@ +--- +title: Module hilbish +description: the core Hilbish API +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction +The Hilbish module includes the core API, containing +interfaces and functions which directly relate to shell functionality. + +## Functions +||| +|----|----| +|alias(cmd, orig)|Sets an alias, with a name of `cmd` to another command.| +|appendPath(dir)|Appends the provided dir to the command path (`$PATH`)| +|complete(scope, cb)|Registers a completion handler for the specified scope.| +|cwd() -> string|Returns the current directory of the shell.| +|exec(cmd)|Replaces the currently running Hilbish instance with the supplied command.| +|goro(fn)|Puts `fn` in a Goroutine.| +|highlighter(line)|Line highlighter handler.| +|hinter(line, pos)|The command line hint handler. It gets called on every key insert to| +|inputMode(mode)|Sets the input mode for Hilbish's line reader.| +|interval(cb, time) -> @Timer|Runs the `cb` function every specified amount of `time`.| +|multiprompt(str)|Changes the text prompt when Hilbish asks for more input.| +|prependPath(dir)|Prepends `dir` to $PATH.| +|prompt(str, typ)|Changes the shell prompt to the provided string.| +|read(prompt) -> input (string)|Read input from the user, using Hilbish's line editor/input reader.| +|run(cmd, streams) -> exitCode (number), stdout (string), stderr (string)|Runs `cmd` in Hilbish's shell script interpreter.| +|runnerMode(mode)|Sets the execution/runner mode for interactive Hilbish.| +|timeout(cb, time) -> @Timer|Executed the `cb` function after a period of `time`.| +|which(name) -> string|Checks if `name` is a valid command.| + +## Static module fields +||| +|----|----| +|ver|The version of Hilbish| +|goVersion|The version of Go that Hilbish was compiled with| +|user|Username of the user| +|host|Hostname of the machine| +|dataDir|Directory for Hilbish data files, including the docs and default modules| +|interactive|Is Hilbish in an interactive shell?| +|login|Is Hilbish the login shell?| +|vimMode|Current Vim input mode of Hilbish (will be nil if not in Vim input mode)| +|exitCode|Exit code of the last executed command| + +
+
+

+hilbish.alias(cmd, orig) + + + +

+ +Sets an alias, with a name of `cmd` to another command. + +#### Parameters +`string` **`cmd`** +Name of the alias + +`string` **`orig`** +Command that will be aliased + +#### Example +```lua +-- With this, "ga file" will turn into "git add file" +hilbish.alias('ga', 'git add') + +-- Numbered substitutions are supported here! +hilbish.alias('dircount', 'ls %1 | wc -l') +-- "dircount ~" would count how many files are in ~ (home directory). +``` +
+ +
+
+

+hilbish.appendPath(dir) + + + +

+ +Appends the provided dir to the command path (`$PATH`) + +#### Parameters +`string|table` **`dir`** +Directory (or directories) to append to path + +#### Example +```lua +hilbish.appendPath '~/go/bin' +-- Will add ~/go/bin to the command path. + +-- Or do multiple: +hilbish.appendPath { + '~/go/bin', + '~/.local/bin' +} +``` +
+ +
+
+

+hilbish.complete(scope, cb) + + + +

+ +Registers a completion handler for the specified scope. +A `scope` is expected to be `command.`, +replacing with the name of the command (for example `command.git`). +The documentation for completions, under Features/Completions or `doc completions` +provides more details. + +#### Parameters +`string` **`scope`** + + +`function` **`cb`** + + +#### Example +```lua +-- This is a very simple example. Read the full doc for completions for details. +hilbish.complete('command.sudo', function(query, ctx, fields) + if #fields == 0 then + -- complete for commands + local comps, pfx = hilbish.completion.bins(query, ctx, fields) + local compGroup = { + items = comps, -- our list of items to complete + type = 'grid' -- what our completions will look like. + } + + return {compGroup}, pfx + end + + -- otherwise just be boring and return files + + local comps, pfx = hilbish.completion.files(query, ctx, fields) + local compGroup = { + items = comps, + type = 'grid' + } + + return {compGroup}, pfx +end) +``` +
+ +
+
+

+hilbish.cwd() -> string + + + +

+ +Returns the current directory of the shell. + +#### Parameters +This function has no parameters. +
+ +
+
+

+hilbish.exec(cmd) + + + +

+ +Replaces the currently running Hilbish instance with the supplied command. +This can be used to do an in-place restart. + +#### Parameters +`string` **`cmd`** + + +
+ +
+
+

+hilbish.goro(fn) + + + +

+ +Puts `fn` in a Goroutine. +This can be used to run any function in another thread at the same time as other Lua code. +**NOTE: THIS FUNCTION MAY CRASH HILBISH IF OUTSIDE VARIABLES ARE ACCESSED.** +**This is a limitation of the Lua runtime.** + +#### Parameters +`function` **`fn`** + + +
+ +
+
+

+hilbish.highlighter(line) + + + +

+ +Line highlighter handler. +This is mainly for syntax highlighting, but in reality could set the input +of the prompt to *display* anything. The callback is passed the current line +and is expected to return a line that will be used as the input display. +Note that to set a highlighter, one has to override this function. + +#### Parameters +`string` **`line`** + + +#### Example +```lua +--This code will highlight all double quoted strings in green. +function hilbish.highlighter(line) + return line:gsub('"%w+"', function(c) return lunacolors.green(c) end) +end +``` +
+ +
+
+

+hilbish.hinter(line, pos) + + + +

+ +The command line hint handler. It gets called on every key insert to +determine what text to use as an inline hint. It is passed the current +line and cursor position. It is expected to return a string which is used +as the text for the hint. This is by default a shim. To set hints, +override this function with your custom handler. + +#### Parameters +`string` **`line`** + + +`number` **`pos`** +Position of cursor in line. Usually equals string.len(line) + +#### Example +```lua +-- this will display "hi" after the cursor in a dimmed color. +function hilbish.hinter(line, pos) + return 'hi' +end +``` +
+ +
+
+

+hilbish.inputMode(mode) + + + +

+ +Sets the input mode for Hilbish's line reader. +`emacs` is the default. Setting it to `vim` changes behavior of input to be +Vim-like with modes and Vim keybinds. + +#### Parameters +`string` **`mode`** +Can be set to either `emacs` or `vim` + +
+ +
+
+

+hilbish.interval(cb, time) -> Timer + + + +

+ +Runs the `cb` function every specified amount of `time`. +This creates a timer that ticking immediately. + +#### Parameters +`function` **`cb`** + + +`number` **`time`** +Time in milliseconds. + +
+ +
+
+

+hilbish.multiprompt(str) + + + +

+ +Changes the text prompt when Hilbish asks for more input. +This will show up when text is incomplete, like a missing quote + +#### Parameters +`string` **`str`** + + +#### Example +```lua +--[[ +imagine this is your text input: +user ~ ∆ echo "hey + +but there's a missing quote! hilbish will now prompt you so the terminal +will look like: +user ~ ∆ echo "hey +--> ...!" + +so then you get +user ~ ∆ echo "hey +--> ...!" +hey ...! +]]-- +hilbish.multiprompt '-->' +``` +
+ +
+
+

+hilbish.prependPath(dir) + + + +

+ +Prepends `dir` to $PATH. + +#### Parameters +`string` **`dir`** + + +
+ +
+
+

+hilbish.prompt(str, typ) + + + +

+ +Changes the shell prompt to the provided string. +There are a few verbs that can be used in the prompt text. +These will be formatted and replaced with the appropriate values. +`%d` - Current working directory +`%u` - Name of current user +`%h` - Hostname of device + +#### Parameters +`string` **`str`** + + +`string` **`typ?`** +Type of prompt, being left or right. Left by default. + +#### Example +```lua +-- the default hilbish prompt without color +hilbish.prompt '%u %d ∆' +-- or something of old: +hilbish.prompt '%u@%h :%d $' +-- prompt: user@hostname: ~/directory $ +``` +
+ +
+
+

+hilbish.read(prompt) -> input (string) + + + +

+ +Read input from the user, using Hilbish's line editor/input reader. +This is a separate instance from the one Hilbish actually uses. +Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs. + +#### Parameters +`string` **`prompt?`** +Text to print before input, can be empty. + +
+ +
+
+

+hilbish.run(cmd, streams) -> exitCode (number), stdout (string), stderr (string) + + + +

+ +Runs `cmd` in Hilbish's shell script interpreter. +The `streams` parameter specifies the output and input streams the command should use. +For example, to write command output to a sink. +As a table, the caller can directly specify the standard output, error, and input +streams of the command with the table keys `out`, `err`, and `input` respectively. +As a boolean, it specifies whether the command should use standard output or return its output streams. + +#### Parameters +`string` **`cmd`** + + +`table|boolean` **`streams`** + + +#### Example +```lua + +// This code is the same as `ls -l | wc -l` +local fs = require 'fs' +local pr, pw = fs.pipe() +hilbish.run('ls -l', { + stdout = pw, + stderr = pw, +}) + +pw:close() + +hilbish.run('wc -l', { + stdin = pr +}) + +``` +
+ +
+
+

+hilbish.runnerMode(mode) + + + +

+ +Sets the execution/runner mode for interactive Hilbish. +This determines whether Hilbish wll try to run input as Lua +and/or sh or only do one of either. +Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua), +sh, and lua. It also accepts a function, to which if it is passed one +will call it to execute user input instead. +Read [about runner mode](../features/runner-mode) for more information. + +#### Parameters +`string|function` **`mode`** + + +
+ +
+
+

+hilbish.timeout(cb, time) -> Timer + + + +

+ +Executed the `cb` function after a period of `time`. +This creates a Timer that starts ticking immediately. + +#### Parameters +`function` **`cb`** + + +`number` **`time`** +Time to run in milliseconds. + +
+ +
+
+

+hilbish.which(name) -> string + + + +

+ +Checks if `name` is a valid command. +Will return the path of the binary, or a basename if it's a commander. + +#### Parameters +`string` **`name`** + + +
+ +## Types +
+ +## Sink +A sink is a structure that has input and/or output to/from +a desination. + +### Methods +#### autoFlush(auto) +Sets/toggles the option of automatically flushing output. +A call with no argument will toggle the value. + +#### flush() +Flush writes all buffered input to the sink. + +#### read() -> string +Reads a liine of input from the sink. + +#### readAll() -> string +Reads all input from the sink. + +#### write(str) +Writes data to a sink. + +#### writeln(str) +Writes data to a sink with a newline at the end. + diff --git a/docs/api/hilbish/hilbish.aliases.md b/docs/api/hilbish/hilbish.aliases.md new file mode 100644 index 0000000..e0a6f48 --- /dev/null +++ b/docs/api/hilbish/hilbish.aliases.md @@ -0,0 +1,91 @@ +--- +title: Module hilbish.aliases +description: command aliasing +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction +The alias interface deals with all command aliases in Hilbish. + +## Functions +||| +|----|----| +|add(alias, cmd)|This is an alias (ha) for the [hilbish.alias](../#alias) function.| +|delete(name)|Removes an alias.| +|list() -> table[string, string]|Get a table of all aliases, with string keys as the alias and the value as the command.| +|resolve(alias) -> string?|Resolves an alias to its original command. Will thrown an error if the alias doesn't exist.| + +
+
+

+hilbish.aliases.add(alias, cmd) + + + +

+ +This is an alias (ha) for the [hilbish.alias](../#alias) function. + +#### Parameters +This function has no parameters. +
+ +
+
+

+hilbish.aliases.delete(name) + + + +

+ +Removes an alias. + +#### Parameters +`string` **`name`** + + +
+ +
+
+

+hilbish.aliases.list() -> table[string, string] + + + +

+ +Get a table of all aliases, with string keys as the alias and the value as the command. + +#### Parameters +This function has no parameters. +#### Example +```lua +hilbish.aliases.add('hi', 'echo hi') + +local aliases = hilbish.aliases.list() +-- -> {hi = 'echo hi'} +``` +
+ +
+
+

+hilbish.aliases.resolve(alias) -> string? + + + +

+ +Resolves an alias to its original command. Will thrown an error if the alias doesn't exist. + +#### Parameters +`string` **`alias`** + + +
+ diff --git a/docs/api/hilbish/hilbish.completion.md b/docs/api/hilbish/hilbish.completion.md new file mode 100644 index 0000000..be6c094 --- /dev/null +++ b/docs/api/hilbish/hilbish.completion.md @@ -0,0 +1,149 @@ +--- +title: Module hilbish.completion +description: tab completions +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction +The completions interface deals with tab completions. + +## Functions +||| +|----|----| +|bins(query, ctx, fields) -> entries (table), prefix (string)|Return binaries/executables based on the provided parameters.| +|call(name, query, ctx, fields) -> completionGroups (table), prefix (string)|Calls a completer function. This is mainly used to call a command completer, which will have a `name`| +|files(query, ctx, fields) -> entries (table), prefix (string)|Returns file matches based on the provided parameters.| +|handler(line, pos)|This function contains the general completion handler for Hilbish. This function handles| + +
+
+

+hilbish.completion.bins(query, ctx, fields) -> entries (table), prefix (string) + + + +

+ +Return binaries/executables based on the provided parameters. +This function is meant to be used as a helper in a command completion handler. + +#### Parameters +`string` **`query`** + + +`string` **`ctx`** + + +`table` **`fields`** + + +#### Example +```lua +-- an extremely simple completer for sudo. +hilbish.complete('command.sudo', function(query, ctx, fields) + table.remove(fields, 1) + if #fields[1] then + -- return commands because sudo runs a command as root..! + + local entries, pfx = hilbish.completion.bins(query, ctx, fields) + return { + type = 'grid', + items = entries + }, pfx + end + + -- ... else suggest files or anything else .. +end) +``` +
+ +
+
+

+hilbish.completion.call(name, query, ctx, fields) -> completionGroups (table), prefix (string) + + + +

+ +Calls a completer function. This is mainly used to call a command completer, which will have a `name` +in the form of `command.name`, example: `command.git`. +You can check the Completions doc or `doc completions` for info on the `completionGroups` return value. + +#### Parameters +`string` **`name`** + + +`string` **`query`** + + +`string` **`ctx`** + + +`table` **`fields`** + + +
+ +
+
+

+hilbish.completion.files(query, ctx, fields) -> entries (table), prefix (string) + + + +

+ +Returns file matches based on the provided parameters. +This function is meant to be used as a helper in a command completion handler. + +#### Parameters +`string` **`query`** + + +`string` **`ctx`** + + +`table` **`fields`** + + +
+ +
+
+

+hilbish.completion.handler(line, pos) + + + +

+ +This function contains the general completion handler for Hilbish. This function handles +completion of everything, which includes calling other command handlers, binaries, and files. +This function can be overriden to supply a custom handler. Note that alias resolution is required to be done in this function. + +#### Parameters +`string` **`line`** +The current Hilbish command line + +`number` **`pos`** +Numerical position of the cursor + +#### Example +```lua +-- stripped down version of the default implementation +function hilbish.completion.handler(line, pos) + local query = fields[#fields] + + if #fields == 1 then + -- call bins handler here + else + -- call command completer or files completer here + end +end +``` +
+ diff --git a/docs/api/hilbish/hilbish.editor.md b/docs/api/hilbish/hilbish.editor.md new file mode 100644 index 0000000..c70b605 --- /dev/null +++ b/docs/api/hilbish/hilbish.editor.md @@ -0,0 +1,103 @@ +--- +title: Module hilbish.editor +description: interactions for Hilbish's line reader +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction +The hilbish.editor interface provides functions to +directly interact with the line editor in use. + +## Functions +||| +|----|----| +|getLine() -> string|Returns the current input line.| +|getVimRegister(register) -> string|Returns the text that is at the register.| +|insert(text)|Inserts text into the Hilbish command line.| +|getChar() -> string|Reads a keystroke from the user. This is in a format of something like Ctrl-L.| +|setVimRegister(register, text)|Sets the vim register at `register` to hold the passed text.| + +
+
+

+hilbish.editor.getLine() -> string + + + +

+ +Returns the current input line. + +#### Parameters +This function has no parameters. +
+ +
+
+

+hilbish.editor.getVimRegister(register) -> string + + + +

+ +Returns the text that is at the register. + +#### Parameters +`string` **`register`** + + +
+ +
+
+

+hilbish.editor.insert(text) + + + +

+ +Inserts text into the Hilbish command line. + +#### Parameters +`string` **`text`** + + +
+ +
+
+

+hilbish.editor.getChar() -> string + + + +

+ +Reads a keystroke from the user. This is in a format of something like Ctrl-L. + +#### Parameters +This function has no parameters. +
+ +
+
+

+hilbish.editor.setVimRegister(register, text) + + + +

+ +Sets the vim register at `register` to hold the passed text. + +#### Parameters +`string` **`text`** + + +
+ diff --git a/docs/api/hilbish/hilbish.history.md b/docs/api/hilbish/hilbish.history.md new file mode 100644 index 0000000..6de9bdf --- /dev/null +++ b/docs/api/hilbish/hilbish.history.md @@ -0,0 +1,102 @@ +--- +title: Module hilbish.history +description: command history +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction +The history interface deals with command history. +This includes the ability to override functions to change the main +method of saving history. + +## Functions +||| +|----|----| +|add(cmd)|Adds a command to the history.| +|all() -> table|Retrieves all history as a table.| +|clear()|Deletes all commands from the history.| +|get(index)|Retrieves a command from the history based on the `index`.| +|size() -> number|Returns the amount of commands in the history.| + +
+
+

+hilbish.history.add(cmd) + + + +

+ +Adds a command to the history. + +#### Parameters +`string` **`cmd`** + + +
+ +
+
+

+hilbish.history.all() -> table + + + +

+ +Retrieves all history as a table. + +#### Parameters +This function has no parameters. +
+ +
+
+

+hilbish.history.clear() + + + +

+ +Deletes all commands from the history. + +#### Parameters +This function has no parameters. +
+ +
+
+

+hilbish.history.get(index) + + + +

+ +Retrieves a command from the history based on the `index`. + +#### Parameters +`number` **`index`** + + +
+ +
+
+

+hilbish.history.size() -> number + + + +

+ +Returns the amount of commands in the history. + +#### Parameters +This function has no parameters. +
+ diff --git a/docs/api/hilbish/hilbish.jobs.md b/docs/api/hilbish/hilbish.jobs.md new file mode 100644 index 0000000..fe3978f --- /dev/null +++ b/docs/api/hilbish/hilbish.jobs.md @@ -0,0 +1,146 @@ +--- +title: Module hilbish.jobs +description: background job management +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction + +Manage interactive jobs in Hilbish via Lua. + +Jobs are the name of background tasks/commands. A job can be started via +interactive usage or with the functions defined below for use in external runners. + +## Functions +||| +|----|----| +|add(cmdstr, args, execPath)|Creates a new job. This function does not run the job. This function is intended to be| +|all() -> table[@Job]|Returns a table of all job objects.| +|disown(id)|Disowns a job. This simply deletes it from the list of jobs without stopping it.| +|get(id) -> @Job|Get a job object via its ID.| +|last() -> @Job|Returns the last added job to the table.| + +
+
+

+hilbish.jobs.add(cmdstr, args, execPath) + + + +

+ +Creates a new job. This function does not run the job. This function is intended to be +used by runners, but can also be used to create jobs via Lua. Commanders cannot be ran as jobs. + +#### Parameters +`string` **`cmdstr`** +String that a user would write for the job + +`table` **`args`** +Arguments for the commands. Has to include the name of the command. + +`string` **`execPath`** +Binary to use to run the command. Needs to be an absolute path. + +#### Example +```lua +hilbish.jobs.add('go build', {'go', 'build'}, '/usr/bin/go') +``` +
+ +
+
+

+hilbish.jobs.all() -> table[Job] + + + +

+ +Returns a table of all job objects. + +#### Parameters +This function has no parameters. +
+ +
+
+

+hilbish.jobs.disown(id) + + + +

+ +Disowns a job. This simply deletes it from the list of jobs without stopping it. + +#### Parameters +`number` **`id`** + + +
+ +
+
+

+hilbish.jobs.get(id) -> Job + + + +

+ +Get a job object via its ID. + +#### Parameters +This function has no parameters. +
+ +
+
+

+hilbish.jobs.last() -> Job + + + +

+ +Returns the last added job to the table. + +#### Parameters +This function has no parameters. +
+ +## Types +
+ +## Job +The Job type describes a Hilbish job. +## Object properties +||| +|----|----| +|cmd|The user entered command string for the job.| +|running|Whether the job is running or not.| +|id|The ID of the job in the job table| +|pid|The Process ID| +|exitCode|The last exit code of the job.| +|stdout|The standard output of the job. This just means the normal logs of the process.| +|stderr|The standard error stream of the process. This (usually) includes error messages of the job.| + + +### Methods +#### background() +Puts a job in the background. This acts the same as initially running a job. + +#### foreground() +Puts a job in the foreground. This will cause it to run like it was +executed normally and wait for it to complete. + +#### start() +Starts running the job. + +#### stop() +Stops the job from running. + diff --git a/docs/api/hilbish/hilbish.module.md b/docs/api/hilbish/hilbish.module.md new file mode 100644 index 0000000..4029d7a --- /dev/null +++ b/docs/api/hilbish/hilbish.module.md @@ -0,0 +1,73 @@ +--- +title: Module hilbish.module +description: native module loading +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction + +The hilbish.module interface provides a function to load +Hilbish plugins/modules. Hilbish modules are Go-written +plugins (see https://pkg.go.dev/plugin) that are used to add functionality +to Hilbish that cannot be written in Lua for any reason. + +Note that you don't ever need to use the load function that is here as +modules can be loaded with a `require` call like Lua C modules, and the +search paths can be changed with the `paths` property here. + +To make a valid native module, the Go plugin has to export a Loader function +with a signature like so: `func(*rt.Runtime) rt.Value`. + +`rt` in this case refers to the Runtime type at +https://pkg.go.dev/github.com/arnodel/golua@master/runtime#Runtime + +Hilbish uses this package as its Lua runtime. You will need to read +it to use it for a native plugin. + +Here is some code for an example plugin: +```go +package main + +import ( + rt "github.com/arnodel/golua/runtime" +) + +func Loader(rtm *rt.Runtime) rt.Value { + return rt.StringValue("hello world!") +} +``` + +This can be compiled with `go build -buildmode=plugin plugin.go`. +If you attempt to require and print the result (`print(require 'plugin')`), it will show "hello world!" + +## Functions +||| +|----|----| +|load(path)|Loads a module at the designated `path`.| + +## Static module fields +||| +|----|----| +|paths|A list of paths to search when loading native modules. This is in the style of Lua search paths and will be used when requiring native modules. Example: `?.so;?/?.so`| + +
+
+

+hilbish.module.load(path) + + + +

+ +Loads a module at the designated `path`. +It will throw if any error occurs. + +#### Parameters +`string` **`path`** + + +
+ diff --git a/docs/api/hilbish/hilbish.os.md b/docs/api/hilbish/hilbish.os.md new file mode 100644 index 0000000..13b56b0 --- /dev/null +++ b/docs/api/hilbish/hilbish.os.md @@ -0,0 +1,20 @@ +--- +title: Module hilbish.os +description: operating system info +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction +Provides simple text information properties about the current operating system. +This mainly includes the name and version. + +## Static module fields +||| +|----|----| +|family|Family name of the current OS| +|name|Pretty name of the current OS| +|version|Version of the current OS| + diff --git a/docs/api/hilbish/hilbish.runner.md b/docs/api/hilbish/hilbish.runner.md new file mode 100644 index 0000000..b5cfde4 --- /dev/null +++ b/docs/api/hilbish/hilbish.runner.md @@ -0,0 +1,114 @@ +--- +title: Module hilbish.runner +description: interactive command runner customization +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction + The runner interface contains functions that allow the user to change +how Hilbish interprets interactive input. +Users can add and change the default runner for interactive input to any +language or script of their choosing. A good example is using it to +write command in Fennel. + +Runners are functions that evaluate user input. The default runners in +Hilbish can run shell script and Lua code. + +A runner is passed the input and has to return a table with these values. +All are not required, only the useful ones the runner needs to return. +(So if there isn't an error, just omit `err`.) + +- `exitCode` (number): A numerical code to indicate the exit result. +- `input` (string): The user input. This will be used to add +to the history. +- `err` (string): A string to indicate an interal error for the runner. +It can be set to a few special values for Hilbish to throw the right hooks and have a better looking message: + +`[command]: not-found` will throw a command.not-found hook based on what `[command]` is. + +`[command]: not-executable` will throw a command.not-executable hook. +- `continue` (boolean): Whether to prompt the user for more input. + +Here is a simple example of a fennel runner. It falls back to +shell script if fennel eval has an error. +```lua +local fennel = require 'fennel' + +hilbish.runnerMode(function(input) + local ok = pcall(fennel.eval, input) + if ok then + return { + input = input + } + end + + return hilbish.runner.sh(input) +end) +``` + +## Functions +||| +|----|----| +|setMode(cb)|This is the same as the `hilbish.runnerMode` function.| +|lua(cmd)|Evaluates `cmd` as Lua input. This is the same as using `dofile`| +|sh(cmd)|Runs a command in Hilbish's shell script interpreter.| + +
+
+

+hilbish.runner.setMode(cb) + + + +

+ +This is the same as the `hilbish.runnerMode` function. +It takes a callback, which will be used to execute all interactive input. +In normal cases, neither callbacks should be overrided by the user, +as the higher level functions listed below this will handle it. + +#### Parameters +`function` **`cb`** + + +
+ +
+
+

+hilbish.runner.lua(cmd) + + + +

+ +Evaluates `cmd` as Lua input. This is the same as using `dofile` +or `load`, but is appropriated for the runner interface. + +#### Parameters +`string` **`cmd`** + + +
+ +
+
+

+hilbish.runner.sh(cmd) + + + +

+ +Runs a command in Hilbish's shell script interpreter. +This is the equivalent of using `source`. + +#### Parameters +`string` **`cmd`** + + +
+ diff --git a/docs/api/hilbish/hilbish.timers.md b/docs/api/hilbish/hilbish.timers.md new file mode 100644 index 0000000..f218d2b --- /dev/null +++ b/docs/api/hilbish/hilbish.timers.md @@ -0,0 +1,100 @@ +--- +title: Module hilbish.timers +description: timeout and interval API +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction + +If you ever want to run a piece of code on a timed interval, or want to wait +a few seconds, you don't have to rely on timing tricks, as Hilbish has a +timer API to set intervals and timeouts. + +These are the simple functions `hilbish.interval` and `hilbish.timeout` (doc +accessible with `doc hilbish`, or `Module hilbish` on the Website). + +An example of usage: +```lua +local t = hilbish.timers.create(hilbish.timers.TIMEOUT, 5000, function() + print 'hello!' +end) + +t:start() +print(t.running) // true +``` + +## Functions +||| +|----|----| +|create(type, time, callback) -> @Timer|Creates a timer that runs based on the specified `time`.| +|get(id) -> @Timer|Retrieves a timer via its ID.| + +## Static module fields +||| +|----|----| +|INTERVAL|Constant for an interval timer type| +|TIMEOUT|Constant for a timeout timer type| + +
+
+

+hilbish.timers.create(type, time, callback) -> Timer + + + +

+ +Creates a timer that runs based on the specified `time`. + +#### Parameters +`number` **`type`** +What kind of timer to create, can either be `hilbish.timers.INTERVAL` or `hilbish.timers.TIMEOUT` + +`number` **`time`** +The amount of time the function should run in milliseconds. + +`function` **`callback`** +The function to run for the timer. + +
+ +
+
+

+hilbish.timers.get(id) -> Timer + + + +

+ +Retrieves a timer via its ID. + +#### Parameters +`number` **`id`** + + +
+ +## Types +
+ +## Timer +The Job type describes a Hilbish timer. +## Object properties +||| +|----|----| +|type|What type of timer it is| +|running|If the timer is running| +|duration|The duration in milliseconds that the timer will run| + + +### Methods +#### start() +Starts a timer. + +#### stop() +Stops a timer. + diff --git a/docs/api/hilbish/hilbish.userDir.md b/docs/api/hilbish/hilbish.userDir.md new file mode 100644 index 0000000..a2b7337 --- /dev/null +++ b/docs/api/hilbish/hilbish.userDir.md @@ -0,0 +1,20 @@ +--- +title: Module hilbish.userDir +description: user-related directories +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction +This interface just contains properties to know about certain user directories. +It is equivalent to XDG on Linux and gets the user's preferred directories +for configs and data. + +## Static module fields +||| +|----|----| +|config|The user's config directory| +|data|The user's directory for program data| + diff --git a/docs/api/terminal.md b/docs/api/terminal.md new file mode 100644 index 0000000..1bd4cc1 --- /dev/null +++ b/docs/api/terminal.md @@ -0,0 +1,81 @@ +--- +title: Module terminal +description: low level terminal library +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction +The terminal library is a simple and lower level library for certain terminal interactions. + +## Functions +||| +|----|----| +|restoreState()|Restores the last saved state of the terminal| +|saveState()|Saves the current state of the terminal.| +|setRaw()|Puts the terminal into raw mode.| +|size()|Gets the dimensions of the terminal. Returns a table with `width` and `height`| + +
+
+

+terminal.restoreState() + + + +

+ +Restores the last saved state of the terminal + +#### Parameters +This function has no parameters. +
+ +
+
+

+terminal.saveState() + + + +

+ +Saves the current state of the terminal. + +#### Parameters +This function has no parameters. +
+ +
+
+

+terminal.setRaw() + + + +

+ +Puts the terminal into raw mode. + +#### Parameters +This function has no parameters. +
+ +
+
+

+terminal.size() + + + +

+ +Gets the dimensions of the terminal. Returns a table with `width` and `height` +NOTE: The size refers to the amount of columns and rows of text that can fit in the terminal. + +#### Parameters +This function has no parameters. +
+ diff --git a/docs/bait.txt b/docs/bait.txt deleted file mode 100644 index fdc712f..0000000 --- a/docs/bait.txt +++ /dev/null @@ -1,10 +0,0 @@ -catch(name, cb) > Catches a hook with `name`. Runs the `cb` when it is thrown - -catchOnce(name, cb) > Same as catch, but only runs the `cb` once and then removes the hook - -release(name, catcher) > Removes the `catcher` for the event with `name` -For this to work, `catcher` has to be the same function used to catch -an event, like one saved to a variable. - -throw(name, ...args) > Throws a hook with `name` with the provided `args` - diff --git a/docs/commander.txt b/docs/commander.txt deleted file mode 100644 index 8b4b329..0000000 --- a/docs/commander.txt +++ /dev/null @@ -1,4 +0,0 @@ -deregister(name) > Deregisters any command registered with `name` - -register(name, cb) > Register a command with `name` that runs `cb` when ran - diff --git a/docs/completions.md b/docs/completions.md new file mode 100644 index 0000000..59ead1b --- /dev/null +++ b/docs/completions.md @@ -0,0 +1,78 @@ +--- +title: Completions +description: Tab completion for commands. +layout: doc +menu: + docs: + parent: "Features" +--- + +Completions for commands can be created with the [`hilbish.complete`](../api/hilbish#complete) +function. See the link for how to use it. + +To create completions for a command is simple. +The callback will be passed 3 parameters: +- `query` (string): The text that the user is currently trying to complete. +This should be used to match entries. +- `ctx` (string): Contains the entire line. Use this if +more text is needed to be parsed for context. +- `fields` (string): The `ctx` split up by spaces. + +In most cases, the completer just uses `fields` to check the amount +and `query` on what to match entries on. + +In order to return your results, it has to go within a "completion group." +Then you return a table of completion groups and a prefix. The prefix will +usually just be the `query`. + +Hilbish allows one to mix completion menus of different types, so +a grid menu and a list menu can be used and complete and display at the same time. +A completion group is a table with these keys: +- `type` (string): type of completion menu, either `grid` or `list`. +- `items` (table): a list of items. + +The requirements of the `items` table is different based on the +`type`. If it is a `grid`, it can simply be a table of strings. + +Otherwise if it is a `list` then each entry can +either be a string or a table. +Example: +```lua +local cg = { + items = { + 'list item 1', + ['--command-flag-here'] = {'this does a thing', '--the-flag-alias'} + }, + type = 'list' +} +local cg2 = { + items = {'just', 'a bunch', 'of items', 'here', 'hehe'}, + type = 'grid' +} + +return {cg, cg2}, prefix +``` + +Which looks like this: +{{< video src="https://safe.saya.moe/t4CiLK6dgPbD.mp4" >}} + +# Completion Handler +Like most parts of Hilbish, it's made to be extensible and +customizable. The default handler for completions in general can +be overwritten to provide more advanced completions if needed. +This usually doesn't need to be done though, unless you know +what you're doing. + +The default completion handler provides 3 things: +binaries (with a plain name requested to complete, those in +$PATH), files, or command completions. It will try to run a handler +for the command or fallback to file completions. + +To overwrite it, just assign a function to `hilbish.completion.handler` like so: +```lua +-- line is the entire line as a string +-- pos is the position of the cursor. +function hilbish.completion.handler(line, pos) + -- do things +end +``` diff --git a/docs/completions.txt b/docs/completions.txt deleted file mode 100644 index 1354dc0..0000000 --- a/docs/completions.txt +++ /dev/null @@ -1,44 +0,0 @@ -Hilbish has a pretty good completion system. It has a nice looking menu, -with 2 types of menus: grid (like file completions) or list. - -Like most parts of Hilbish, it's made to be extensible and customizable. -The default handler for completions in general can be overwritten to provide -more advanced completions if needed. - -# Completion Handler -By default, it provides 3 things: for the first argument, binaries (with a -plain name requested to complete, those in $PATH), files, or command -completions. With the default completion handler, it will try to run a -handler for the command or fallback to file completions. - -To overwrite it, just assign a function to `hilbish.completion.handler` -like so: -function hilbish.completion.handler(line, pos) - -- do things -end -It is passed 2 arguments, the entire line, and the current cursor position. -The functions in the completion interface take 3 arguments: query, ctx, -and fields. The `query`, which what the user is currently trying to complete, -`ctx`, being just the entire line, and `fields` being a table of arguments. -It's just `ctx` split up, delimited by spaces. -It's expected to return 2 things: a table of completion groups, and a prefix. -A completion group is defined as a table with 2 keys: `items` and `type`. -The `items` field is just a table of items to use for completions. -The `type` is for the completion menu type, being either `grid` or `list`. -The prefix is what all the completions start with. It should be empty -if the user doesn't have a query. If the beginning of the completion -item does not match the prefix, it will be replaced and fixed properly -in the line. It is case sensitive. - -If you want to overwrite the functionality of the general completion handler, -or make your command completion have files as well (and filter them), -then there is the `files` function, which is mentioned below. - -# Completion Interface -## Functions -- `files(query, ctx, fields)` -> table, prefix: get file completions, based -on the user's query. -- `bins(query, ctx, fields)` -> table, prefix: get binary/executable -completions, based on user query. -- `call(scope, query, ctx, fields)` -> table, prefix: call a completion handler -with `scope`, usually being in the form of `command.` diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..f89f269 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,26 @@ +--- +title: Frequently Asked Questions +layout: doc +weight: -20 +menu: docs +--- + +# Is Hilbish POSIX compliant? +No, it is not. POSIX compliance is a non-goal. Perhaps in the future, +someone would be able to write a native plugin to support shell scripting +(which would be against it's main goal, but ....) + +# Windows Support? +It compiles for Windows (CI ensures it does), but otherwise it is not +directly supported. If you'd like to improve this situation, +checkout [the discussion](https://github.com/Rosettea/Hilbish/discussions/165). + +# Why? +Hilbish emerged from the desire of a Lua configured shell. +It was the initial reason that it was created, but now it's more: +to be hyper extensible, simpler and more user friendly. + +# Does it have "autocompletion" or "tab completion" +Of course! This is a modern shell. Hilbish provides a way for users +to write tab completion for any command and/or the whole shell. +Inline hinting and syntax highlighting are also available. diff --git a/docs/features/_index.md b/docs/features/_index.md new file mode 100644 index 0000000..21e2fd5 --- /dev/null +++ b/docs/features/_index.md @@ -0,0 +1,11 @@ +--- +title: Features +layout: doc +weight: -40 +menu: docs +--- + +Hilbish has a wide range of features to enhance the user's experience +new ones are always being added. If there is something missing here or +something you would like to see, please [start a discussion](https://github.com/Rosettea/Hilbish/discussions) +or comment on any existing ones which match your request. diff --git a/docs/features/notifications.md b/docs/features/notifications.md new file mode 100644 index 0000000..c3a9b53 --- /dev/null +++ b/docs/features/notifications.md @@ -0,0 +1,39 @@ +--- +title: Notification +description: Get notified of shell actions. +layout: doc +menu: + docs: + parent: "Features" +--- + +Hilbish features a simple notification system which can be +used by other plugins and parts of the shell to notify the user +of various actions. This is used via the `hilbish.message` interface. + +A `message` is defined as a table with the following properties: +- `icon`: A unicode/emoji icon for the notification. +- `title`: The title of the message +- `text`: Message text/body +- `channel`: The source of the message. This should be a +unique and easily readable text identifier. +- `summary`: A short summary of the notification and message. +If this is not present and you are using this to display messages, +you should take part of the `text` instead. + +The `hilbish.message` interface provides the following functions: +- `send(message)`: Sends a message and emits the `hilbish.notification` +signal. DO NOT emit the `hilbish.notification` signal directly, or +the message will not be stored by the message handler. +- `read(idx)`: Marks message at `idx` as read. +- `delete(idx)`: Removes message at `idx`. +- `readAll()`: Marks all messages as read. +- `clear()`: Deletes all messages. + +There are a few simple use cases of this notification/messaging system. +It could also be used as some "inter-shell" messaging system (???) but +is intended to display to users. + +An example is notifying users of completed jobs/commands ran in the background. +Any Hilbish-native command (think the upcoming Greenhouse pager) can display +it. diff --git a/docs/features/opts.md b/docs/features/opts.md new file mode 100644 index 0000000..2fb848d --- /dev/null +++ b/docs/features/opts.md @@ -0,0 +1,78 @@ +--- +title: Options +description: Simple customizable options. +layout: doc +menu: + docs: + parent: "Features" +--- + +Opts are simple toggle or value options a user can set in Hilbish. +As toggles, there are things like `autocd` or history saving. As values, +there is the `motd` which the user can either change to a custom string or disable. + +Opts are accessed from the `hilbish.opts` table. Here they can either +be read or modified + +### `autocd` +#### Value: `boolean` +#### Default: `false` + +The autocd opt makes it so that lone directories attempted to be executed are +instead set as the shell's directory. + +Example: +``` +~/Directory +∆ ~ +~ +∆ Downloads +~/Downloads +∆ ../Documents +~/Documents +∆ +``` + +
+ +### `history` +#### Value: `boolean` +#### Default: `true` +Sets whether command history will be saved or not. + +
+ +### `greeting` +#### Value: `boolean` or `string` +The greeting is the message that Hilbish shows on startup +(the one which says Welcome to Hilbish). + +This can be set to either true/false to enable/disable or a custom greeting string. + +
+ +### `motd` +#### Value: `boolean` +#### Default: `true` +The message of the day shows the current major.minor version and +includes a small range of things added in the current release. + +This can be set to `false` to disable the message. + +
+ +### `fuzzy` +#### Value: `boolean` +#### Default: `false` +Toggles the functionality of fuzzy history searching, usable +via the menu in Ctrl-R. Fuzzy searching is an approximate searching +method, which means results that match *closest* will be shown instead +of an exact match. + +
+ +### `notifyJobFinish` +#### Value: `boolean` +#### Default: `true` +If this is enabled, when a background job is finished, +a [notification](../notifications) will be sent. diff --git a/docs/features/runner-mode.md b/docs/features/runner-mode.md new file mode 100644 index 0000000..0f7a8dd --- /dev/null +++ b/docs/features/runner-mode.md @@ -0,0 +1,66 @@ +--- +title: Runner Mode +description: Customize the interactive script/command runner. +layout: doc +menu: + docs: + parent: "Features" +--- + +Hilbish allows you to change how interactive text can be interpreted. +This is mainly due to the fact that the default method Hilbish uses +is that it runs Lua first and then falls back to shell script. + +In some cases, someone might want to switch to just shell script to avoid +it while interactive but still have a Lua config, or go full Lua to use +Hilbish as a REPL. This also allows users to add alternative languages like +Fennel as the interactive script runner. + +Runner mode can also be used to handle specific kinds of input before +evaluating like normal, which is how [Link.hsh](https://github.com/TorchedSammy/Link.hsh) +handles links. + +The "runner mode" of Hilbish is customizable via `hilbish.runnerMode`, +which determines how Hilbish will run user input. By default, this is +set to `hybrid` which is the previously mentioned behaviour of running Lua +first then going to shell script. If you want the reverse order, you can +set it to `hybridRev` and for isolated modes there is `sh` and `lua` +respectively. + +You can also set it to a function, which will be called everytime Hilbish +needs to run interactive input. For more detail, see the [API documentation](../../api/hilbish/hilbish.runner) + +The `hilbish.runner` interface is an alternative to using `hilbish.runnerMode` +and also provides the shell script and Lua runner functions that Hilbish itself uses. + +A runner function is expected to return a table with the following values: +- `exitCode` (number): Exit code of the command +- `input` (string): The text input of the user. This is used by Hilbish to append extra input, in case +more is requested. +- `err` (string): A string that represents an error from the runner. +This should only be set when, for example, there is a syntax error. +It can be set to a few special values for Hilbish to throw the right +hooks and have a better looking message. + - `: not-found` will throw a `command.not-found` hook + based on what `` is. + - `: not-executable` will throw a `command.not-executable` hook. +- `continue` (boolean): Whether Hilbish should prompt the user for no input + +## Functions +These are the "low level" functions for the `hilbish.runner` interface. + ++ setMode(mode) > The same as `hilbish.runnerMode` ++ sh(input) -> table > Runs `input` in Hilbish's sh interpreter ++ lua(input) -> table > Evals `input` as Lua code + +These functions should be preferred over the previous ones. ++ setCurrent(mode) > The same as `setMode`, but works with runners managed +via the functions below. ++ add(name, runner) > Adds a runner to a table of available runners. The `runner` +argument is either a function or a table with a run callback. ++ set(name, runner) > The same as `add` but requires passing a table and +overwrites if the `name`d runner already exists. ++ get(name) > runner > Gets a runner by name. It is a table with at least a +run function, to run input. ++ exec(cmd, runnerName) > Runs `cmd` with a runner. If `runnerName` isn't passed, +the current runner mode is used. diff --git a/docs/fs.txt b/docs/fs.txt deleted file mode 100644 index 8372afd..0000000 --- a/docs/fs.txt +++ /dev/null @@ -1,22 +0,0 @@ -abs(path) > Gives an absolute version of `path`. - -basename(path) > Gives the basename of `path`. For the rules, -see Go's filepath.Base - -cd(dir) > Changes directory to `dir` - -dir(path) > Returns the directory part of `path`. For the rules, see Go's -filepath.Dir - -glob(pattern) > Glob all files and directories that match the pattern. -For the rules, see Go's filepath.Glob - -join(paths...) > Takes paths and joins them together with the OS's -directory separator (forward or backward slash). - -mkdir(name, recursive) > Makes a directory called `name`. If `recursive` is true, it will create its parent directories. - -readdir(dir) > Returns a table of files in `dir` - -stat(path) > Returns info about `path` - diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..de7607e --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,60 @@ +--- +title: Getting Started +layout: doc +weight: -10 +menu: docs +--- + +To start Hilbish, open a terminal. If Hilbish has been installed and is not the +default shell, you can simply run `hilbish` to start it. This will launch +a normal interactive session. +To exit, you can either run the `exit` command or hit Ctrl+D. + +# Setting as Default +## Login shell +There are a few ways to make Hilbish your default shell. A simple way is +to make it your user/login shell. + +{{< warning `It is not recommended to set Hilbish as your login shell. That +is expected to be a POSIX compliant shell, which Hilbish is not. Though if +you still decide to do it, there will just be a few variables missing in +your environment` >}} + +To do that, simply run `chsh -s /usr/bin/hilbish`. +Some distros (namely Fedora) might have `lchsh` instead, which is used like `lchsh `. +When prompted, you can put the path for Hilbish. + +## Default with terminal +The simpler way is to set the default shell for your terminal. The way of +doing this depends on how your terminal settings are configured. + +## Run after login shell +Some shells (like zsh) have an rc file, like `.zlogin`, which is ran when the shell session +is a login shell. In that file, you can run Hilbish. Example: + +``` +exec hilbish -S -l +``` + +This will replace the shell with Hilbish, set $SHELL to Hilbish and launch it as a login shell. + +# Configuration +Once installation and setup has been done, you can then configure Hilbish. +It is configured and scripted via Lua, so the config file is a Lua file. +You can use any pure Lua library to do whatever you want. + +Hilbish's sample configuration is usually located in `hilbish.dataDir .. '/.hilbishrc.lua'`. +You can print that path via Lua to see what it is: `print(hilbish.dataDir .. '/.hilbishrc.lua')`. +As an example, it will usually will result in `/usr/share/hilbish/.hilbishrc.lua` on Linux. + +To edit your user configuration, you can copy that file to `hilbish.userDir.config .. '/hilbish/init.lua'`, +which follows XDG on Linux and MacOS, and is located in %APPDATA% on Windows. + +As the directory is usually `~/.config` on Linux, you can run this command to copy it: +`cp /usr/share/hilbish/.hilbishrc.lua ~/.config/hilbish/init.lua` + +Now you can get to editing it. Since it's just a Lua file, having basic +knowledge of Lua would help. All of Lua's standard libraries and functions +from Lua 5.4 are available. Hilbish has some custom and modules that are +available. To see them, you can run the `doc` command. This also works as +general documentation for other things. diff --git a/docs/hilbish.txt b/docs/hilbish.txt deleted file mode 100644 index d9763a0..0000000 --- a/docs/hilbish.txt +++ /dev/null @@ -1,62 +0,0 @@ -alias(cmd, orig) > Sets an alias of `cmd` to `orig` - -appendPath(dir) > Appends `dir` to $PATH - -complete(scope, cb) > Registers a completion handler for `scope`. -A `scope` is currently only expected to be `command.`, -replacing with the name of the command (for example `command.git`). -`cb` must be a function that returns a table of "completion groups." -Check `doc completions` for more information. - -cwd() > Returns the current directory of the shell - -exec(cmd) > Replaces running hilbish with `cmd` - -goro(fn) > Puts `fn` in a goroutine - -highlighter(line) > Line highlighter handler. This is mainly for syntax highlighting, but in -reality could set the input of the prompt to *display* anything. The -callback is passed the current line and is expected to return a line that -will be used as the input display. - -hinter(line, pos) > The command line hint handler. It gets called on every key insert to -determine what text to use as an inline hint. It is passed the current -line and cursor position. It is expected to return a string which is used -as the text for the hint. This is by default a shim. To set hints, -override this function with your custom handler. - -inputMode(mode) > Sets the input mode for Hilbish's line reader. Accepts either emacs or vim - -interval(cb, time) > Runs the `cb` function every `time` milliseconds. -Returns a `timer` object (see `doc timers`). - -multiprompt(str) > Changes the continued line prompt to `str` - -prependPath(dir) > Prepends `dir` to $PATH - -prompt(str, typ?) > Changes the shell prompt to `str` -There are a few verbs that can be used in the prompt text. -These will be formatted and replaced with the appropriate values. -`%d` - Current working directory -`%u` - Name of current user -`%h` - Hostname of device - -read(prompt) -> input? > Read input from the user, using Hilbish's line editor/input reader. -This is a separate instance from the one Hilbish actually uses. -Returns `input`, will be nil if ctrl + d is pressed, or an error occurs (which shouldn't happen) - -run(cmd, returnOut) -> exitCode, stdout, stderr > Runs `cmd` in Hilbish's sh interpreter. -If returnOut is true, the outputs of `cmd` will be returned as the 2nd and -3rd values instead of being outputted to the terminal. - -runnerMode(mode) > Sets the execution/runner mode for interactive Hilbish. This determines whether -Hilbish wll try to run input as Lua and/or sh or only do one of either. -Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua), -sh, and lua. It also accepts a function, to which if it is passed one -will call it to execute user input instead. - -timeout(cb, time) > Runs the `cb` function after `time` in milliseconds -Returns a `timer` object (see `doc timers`). - -which(name) > Checks if `name` is a valid command - diff --git a/docs/hooks/_index.md b/docs/hooks/_index.md new file mode 100644 index 0000000..ec313c6 --- /dev/null +++ b/docs/hooks/_index.md @@ -0,0 +1,11 @@ +--- +title: Signals +description: +layout: doc +weight: -50 +menu: + docs +--- + +Signals are global events emitted with the [Bait](../api/bait) module. +For more detail on how to use these signals, you may check the Bait page. diff --git a/docs/hooks/command.md b/docs/hooks/command.md new file mode 100644 index 0000000..2e3163e --- /dev/null +++ b/docs/hooks/command.md @@ -0,0 +1,67 @@ +--- +title: Command +description: +layout: doc +menu: + docs: + parent: "Signals" +--- + +## command.preexec +Thrown right before a command is executed. + +#### Variables +`string` **`input`** +The raw string that the user typed. This will include the text +without changes applied to it (argument substitution, alias expansion, +etc.) + +`string` **`cmdStr`** +The command that will be directly executed by the current runner. + +
+ +## command.exit +Thrown after the user's ran command is finished. + +#### Variables +`number` **`code`** +The exit code of what was executed. + +`string` **`cmdStr`** +The command or code that was executed + +
+ +## command.not-found +Thrown if the command attempted to execute was not found. +This can be used to customize the text printed when a command is not found. +Example: +```lua +local bait = require 'bait' +-- Remove any present handlers on `command.not-found` + +local notFoundHooks = bait.hooks 'command.not-found' +for _, hook in ipairs(notFoundHooks) do + bait.release('command.not-found', hook) +end + +-- then assign custom +bait.catch('command.not-found', function(cmd) + print(string.format('The command "%s" was not found.', cmd)) +end) +``` + +#### Variables +`string` **`cmdStr`** +The name of the command. + +
+ +## command.not-executable +Thrown when the user attempts to run a file that is not executable +(like a text file, or Unix binary without +x permission). + +#### Variables +`string` **`cmdStr`** +The name of the command. diff --git a/docs/hooks/command.txt b/docs/hooks/command.txt deleted file mode 100644 index f97f7e3..0000000 --- a/docs/hooks/command.txt +++ /dev/null @@ -1,7 +0,0 @@ -+ `command.exit` -> code, cmdStr > Thrown when a command exits. -`code` is the exit code of the command, and `cmdStr` is the command that was run. - -+ `command.not-found` -> cmdStr > Thrown when a command is not found. - -+ `command.no-perm` -> cmdStr > Thrown when Hilbish attempts to execute a file but -has no permission. diff --git a/docs/hooks/hilbish.md b/docs/hooks/hilbish.md new file mode 100644 index 0000000..d5d8a48 --- /dev/null +++ b/docs/hooks/hilbish.md @@ -0,0 +1,47 @@ +--- +title: Hilbish +description: +layout: doc +menu: + docs: + parent: "Signals" +--- + +## hilbish.exit +Sent when Hilbish is going to exit. + +#### Variables +This signal returns no variables. + +
+ +## hilbish.vimMode +Sent when the Vim mode of Hilbish is changed (like from insert to normal mode). +This can be used to change the prompt and notify based on Vim mode. + +#### Variables +`string` **`modeName`** +The mode that has been set. +Can be these values: `insert`, `normal`, `delete` or `replace` + +
+ +## hilbish.cancel +Sent when the user cancels their command input with Ctrl-C + +#### Variables +This signal returns no variables. + +
+ +## hilbish.notification +Thrown when a [notification](../../features/notifications) is sent. + +#### Variables +`table` **`notification`** +The notification. The properties are defined in the link above. + +
+ ++ `hilbish.vimAction` -> actionName, args > Sent when the user does a "vim action," being something +like yanking or pasting text. See `doc vim-mode actions` for more info. diff --git a/docs/hooks/hilbish.txt b/docs/hooks/hilbish.txt deleted file mode 100644 index d6d5542..0000000 --- a/docs/hooks/hilbish.txt +++ /dev/null @@ -1,7 +0,0 @@ -+ `hilbish.exit` > Sent when Hilbish is about to exit. - -+ `hilbish.vimMode` -> modeName > Sent when Hilbish's Vim mode is changed (example insert to normal mode), -`modeName` is the name of the mode changed to (can be `insert`, `normal`, `delete` or `replace`). - -+ `hilbish.vimAction` -> actionName, args > Sent when the user does a "vim action," being something -like yanking or pasting text. See `doc vim-mode actions` for more info. diff --git a/docs/hooks/index.txt b/docs/hooks/index.txt deleted file mode 100644 index f771543..0000000 --- a/docs/hooks/index.txt +++ /dev/null @@ -1,8 +0,0 @@ -Here is a list of bait hooks that are thrown by Hilbish. If a hook is related -to a command, it will have the `command` scope, as example. - -Here is the format for a doc for a hook: -+ -> > - -`` just means the arguments of the hook. If a hook doc has the format -of `arg...`, it means the hook can take/recieve any number of `arg`. diff --git a/docs/hooks/job.txt b/docs/hooks/job.md similarity index 100% rename from docs/hooks/job.txt rename to docs/hooks/job.md diff --git a/docs/hooks/signal.md b/docs/hooks/signal.md new file mode 100644 index 0000000..63834b0 --- /dev/null +++ b/docs/hooks/signal.md @@ -0,0 +1,40 @@ +--- +title: Signal +description: +layout: doc +menu: + docs: + parent: "Signals" +--- + +## signal.sigint +Thrown when Hilbish receive the SIGINT signal, +aka when Ctrl-C is pressed. + +#### Variables +This signal returns no variables. + +
+ +## signal.resize +Thrown when the terminal is resized. + +#### Variables +This signal returns no variables. + +
+ +## signal.sigusr1 +Thrown when SIGUSR1 is sent to Hilbish. + +#### Variables +This signal returns no variables. + +
+ +## signal.sigusr2 +Thrown when SIGUSR2 is sent to Hilbish. + +#### Variables +This signal returns no variables. + diff --git a/docs/hooks/signal.txt b/docs/hooks/signal.txt deleted file mode 100644 index ac5deed..0000000 --- a/docs/hooks/signal.txt +++ /dev/null @@ -1,7 +0,0 @@ -+ `signal.sigint` > Sent when Hilbish receives SIGINT (on Ctrl-C). - -+ `signal.resize` > Sent when the terminal is resized. - -+ `signal.sigusr1` - -+ `signal.sigusr2` diff --git a/docs/jobs.txt b/docs/jobs.md similarity index 96% rename from docs/jobs.txt rename to docs/jobs.md index a5fee9c..8651051 100644 --- a/docs/jobs.txt +++ b/docs/jobs.md @@ -1,3 +1,5 @@ +(This has mainly been replaced by [hilbish.jobs](../api/hilbish.jobs)). + Hilbish has pretty standard job control. It's missing one or two things, but works well. One thing which is different from other shells (besides Hilbish) itself is the API for jobs, and of course it's in Lua. diff --git a/docs/lunacolors.txt b/docs/lunacolors.md similarity index 94% rename from docs/lunacolors.txt rename to docs/lunacolors.md index e122fef..bde809c 100644 --- a/docs/lunacolors.txt +++ b/docs/lunacolors.md @@ -1,3 +1,10 @@ +--- +title: Lunacolors +layout: doc +weight: -60 +menu: docs +--- + Lunacolors is an ANSI color/styling library for Lua. It is included by default in standard Hilbish distributions to provide easy styling for things like prompts and text. diff --git a/docs/nature/index.txt b/docs/nature/_index.md similarity index 74% rename from docs/nature/index.txt rename to docs/nature/_index.md index 53df6ca..4a3bc35 100644 --- a/docs/nature/index.txt +++ b/docs/nature/_index.md @@ -1,10 +1,17 @@ +--- +title: Nature +layout: doc +weight: -90 +menu: docs +--- + A bit after creation, we have the outside nature. Little plants, seeds, growing to their final phase: a full plant. A lot of Hilbish itself is written in Go, but there are parts made in Lua, being most builtins (`doc`, `cd`, cdr), completions, and other things. -Hilbish's Lua core module is called `nature`. It's handled after everything -on the Go side initializes, which is what that first sentence was from. +Hilbish's Lua core module is called `nature`. +It runs after Hilbish's Go core does. # Nature Modules Currently, `nature` provides 1 intended public module: `nature.dirs`. diff --git a/docs/nature/dirs.md b/docs/nature/dirs.md new file mode 100644 index 0000000..3c707e6 --- /dev/null +++ b/docs/nature/dirs.md @@ -0,0 +1,79 @@ +--- +title: Module dirs +description: No description. +layout: doc +menu: + docs: + parent: "Nature" +--- + +
+
+

+dirs.setOld(d) + + + +

+ +Sets the old directory string. +#### Parameters +`d` **`string`** +
+ +
+
+

+dirs.push() + + + +

+ +Add `d` to the recent directories list. +#### Parameters +This function has no parameters. +
+ +
+
+

+dirs.peak(num) + + + +

+ +Look at `num` amount of recent directories, starting from the latest. +#### Parameters +`num` **`number`** +
+ +
+
+

+dirs.pop(num) + + + +

+ +Remove the specified amount of dirs from the recent directories list. +#### Parameters +`num` **`number`** +
+ +
+
+

+dirs.recent(idx) + + + +

+ +Get entry from recent directories list based on index. +#### Parameters +`idx` **`number`** +
+ diff --git a/docs/runner-mode.txt b/docs/runner-mode.txt deleted file mode 100644 index 5765f18..0000000 --- a/docs/runner-mode.txt +++ /dev/null @@ -1,55 +0,0 @@ -Hilbish is *unique,* when interactive it first attempts to run input as -Lua and then tries shell script. But if you're normal, you wouldn't -really be using Hilbish anyway but you'd also not want this -(or maybe want Lua only in some cases.) - -The "runner mode" of Hilbish is customizable via `hilbish.runnerMode`, -which determines how Hilbish will run user input. By default, this is -set to `hybrid` which is the previously mentioned behaviour of running Lua -first then going to shell script. If you want the reverse order, you can -set it to `hybridRev` and for isolated modes there is `sh` and `lua` -respectively. - -You can also set it to a function, which will be called everytime Hilbish -needs to run interactive input. For example, you can set this to a simple -function to compile and evaluate Fennel, and now you can run Fennel. -You can even mix it with sh to make a hybrid mode with Lua replaced by -Fennel. - -An example: -hilbish.runnerMode(function(input) - local ok = pcall(fennel.eval, input) - if ok then - return input, 0, nil - end - - return hilbish.runner.sh(input) -end) - -The `hilbish.runner` interface is an alternative to using `hilbish.runnerMode` -and also provides the sh and Lua runner functions that Hilbish itself uses. -A runner function is expected to return 3 values: the input, exit code, and an error. -The input return is there incase you need to prompt for more input. -If you don't, just return the input passed to the runner function. -The exit code has to be a number, it will be 0 otherwise and the error can be -`nil` to indicate no error. - -## Functions -These are the "low level" functions for the `hilbish.runner` interface. - -+ setMode(mode) > The same as `hilbish.runnerMode` -+ sh(input) -> input, code, err > Runs `input` in Hilbish's sh interpreter -+ lua(input) -> input, code, err > Evals `input` as Lua code - -The others here are defined in Lua and have EmmyLua documentation. -These functions should be preferred over the previous ones. -+ setCurrent(mode) > The same as `setMode`, but works with runners managed -via the functions below. -+ add(name, runner) > Adds a runner to a table of available runners. The `runner` -argument is either a function or a table with a run callback. -+ set(name, runner) > The same as `add` but requires passing a table and -overwrites if the `name`d runner already exists. -+ get(name) > runner > Gets a runner by name. It is a table with at least a -run function, to run input. -+ exec(cmd, runnerName) > Runs `cmd` with a runner. If `runnerName` isn't passed, -the current runner mode is used. diff --git a/docs/terminal.txt b/docs/terminal.txt deleted file mode 100644 index 7683bbb..0000000 --- a/docs/terminal.txt +++ /dev/null @@ -1,9 +0,0 @@ -restoreState() > Restores the last saved state of the terminal - -saveState() > Saves the current state of the terminal - -setRaw() > Puts the terminal in raw mode - -size() > Gets the dimensions of the terminal. Returns a table with `width` and `height` -Note: this is not the size in relation to the dimensions of the display - diff --git a/docs/timers.md b/docs/timers.md new file mode 100644 index 0000000..1b9c602 --- /dev/null +++ b/docs/timers.md @@ -0,0 +1 @@ +This has been moved to the `hilbish.timers` API doc (accessible by `doc api hilbish.timers`) diff --git a/docs/timers.txt b/docs/timers.txt deleted file mode 100644 index 0f89718..0000000 --- a/docs/timers.txt +++ /dev/null @@ -1,38 +0,0 @@ -If you ever want to run a piece of code on a timed interval, or want to wait -a few seconds, you don't have to rely on timing tricks, as Hilbish has a -timer API to set intervals and timeouts. - -These are the simple functions `hilbish.interval` and `hilbish.timeout` (doc -accessible with `doc hilbish`). But if you want slightly more control over -them, there is the `hilbish.timers` interface. It allows you to get -a timer via ID. - -# Timer Interface -## Functions -- `get(id)` -> timer: get a timer via its id -- `create(type, ms, callback)` -> timer: creates a timer, adding it to the timer pool. -`type` is the type of timer it will be. 0 is an interval, 1 is a timeout. -`ms` is the time it will run for in seconds. callback is the function called -when the timer is triggered. - -# Timer Object -All those previously mentioned functions return a `timer` object, to which -you can stop and start a timer again. - -An example of usage: -local t = hilbish.timers.create(1, 5000, function() - print 'hello!' -end) - -t:stop() -print(t.running, t.duration, t.type) -t:start() - -## Properties -- `duration`: amount of time the timer runs for in milliseconds -- `running`: whether the timer is running or not -- `type`: the type of timer (0 is interval, 1 is timeout) - -## Functions -- `stop()`: stops the timer. returns an error if it's already stopped -- `start()`: starts the timer. returns an error if it's already started diff --git a/docs/vim-mode/index.txt b/docs/vim-mode/_index.md similarity index 76% rename from docs/vim-mode/index.txt rename to docs/vim-mode/_index.md index a30fe74..bda01e9 100644 --- a/docs/vim-mode/index.txt +++ b/docs/vim-mode/_index.md @@ -1,3 +1,10 @@ +--- +title: Vim Mode +layout: doc +weight: -90 +menu: docs +--- + Hilbish has a Vim binding input mode accessible for use. It can be enabled with the `hilbish.inputMode` function (check `doc hilbish`). diff --git a/docs/vim-mode/actions.txt b/docs/vim-mode/actions.md similarity index 88% rename from docs/vim-mode/actions.txt rename to docs/vim-mode/actions.md index 9dfb7b2..9757827 100644 --- a/docs/vim-mode/actions.txt +++ b/docs/vim-mode/actions.md @@ -1,3 +1,12 @@ +--- +title: Actions +layout: doc +weight: -80 +menu: + docs: + parent: "Vim Mode" +--- + Vim actions are essentially just when a user uses a Vim keybind. Things like yanking and pasting are Vim actions. This is not an "offical Vim thing," just a Hilbish thing. diff --git a/editor.go b/editor.go index 868f458..9c49440 100644 --- a/editor.go +++ b/editor.go @@ -6,12 +6,17 @@ import ( rt "github.com/arnodel/golua/runtime" ) +// #interface editor +// interactions for Hilbish's line reader +// The hilbish.editor interface provides functions to +// directly interact with the line editor in use. func editorLoader(rtm *rt.Runtime) *rt.Table { exports := map[string]util.LuaExport{ "insert": {editorInsert, 1, false}, "setVimRegister": {editorSetRegister, 1, false}, "getVimRegister": {editorGetRegister, 2, false}, "getLine": {editorGetLine, 0, false}, + "readChar": {editorReadChar, 0, false}, } mod := rt.NewTable() @@ -20,6 +25,10 @@ func editorLoader(rtm *rt.Runtime) *rt.Table { return mod } +// #interface editor +// insert(text) +// Inserts text into the Hilbish command line. +// #param text string func editorInsert(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -35,6 +44,11 @@ func editorInsert(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } +// #interface editor +// setVimRegister(register, text) +// Sets the vim register at `register` to hold the passed text. +// #aram register string +// #param text string func editorSetRegister(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -55,6 +69,10 @@ func editorSetRegister(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } +// #interface editor +// getVimRegister(register) -> string +// Returns the text that is at the register. +// #param register string func editorGetRegister(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -70,8 +88,21 @@ func editorGetRegister(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext1(t.Runtime, rt.StringValue(string(buf))), nil } +// #interface editor +// getLine() -> string +// Returns the current input line. +// #returns string func editorGetLine(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { buf := lr.rl.GetLine() return c.PushingNext1(t.Runtime, rt.StringValue(string(buf))), nil } + +// #interface editor +// getChar() -> string +// Reads a keystroke from the user. This is in a format of something like Ctrl-L. +func editorReadChar(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + buf := lr.rl.ReadChar() + + return c.PushingNext1(t.Runtime, rt.StringValue(string(buf))), nil +} diff --git a/emmyLuaDocs/bait.lua b/emmyLuaDocs/bait.lua index a5ecebd..c38eea1 100644 --- a/emmyLuaDocs/bait.lua +++ b/emmyLuaDocs/bait.lua @@ -2,24 +2,27 @@ local bait = {} ---- Catches a hook with `name`. Runs the `cb` when it is thrown ---- @param name string ---- @param cb function +--- Catches an event. This function can be used to act on events. +--- +--- function bait.catch(name, cb) end ---- Same as catch, but only runs the `cb` once and then removes the hook ---- @param name string ---- @param cb function +--- Catches an event, but only once. This will remove the hook immediately after it runs for the first time. function bait.catchOnce(name, cb) end ---- Removes the `catcher` for the event with `name` +--- Returns a table of functions that are hooked on an event with the corresponding `name`. +function bait.hooks(name) end + +--- Removes the `catcher` for the event with `name`. --- For this to work, `catcher` has to be the same function used to catch --- an event, like one saved to a variable. -function bait.release() end +--- +--- +function bait.release(name, catcher) end ---- Throws a hook with `name` with the provided `args` ---- @param name string ---- @vararg any -function bait.throw(name, ...) end +--- Throws a hook with `name` with the provided `args`. +--- +--- +function bait.throw(name, ...args) end return bait diff --git a/emmyLuaDocs/commander.lua b/emmyLuaDocs/commander.lua index c6738dd..bfa69e5 100644 --- a/emmyLuaDocs/commander.lua +++ b/emmyLuaDocs/commander.lua @@ -2,13 +2,17 @@ local commander = {} ---- Deregisters any command registered with `name` ---- @param name string +--- Removes the named command. Note that this will only remove Commander-registered commands. function commander.deregister(name) end ---- Register a command with `name` that runs `cb` when ran ---- @param name string ---- @param cb function +--- Adds a new command with the given `name`. When Hilbish has to run a command with a name, +--- it will run the function providing the arguments and sinks. +--- +--- function commander.register(name, cb) end +--- Returns all registered commanders. Returns a list of tables with the following keys: +--- - `exec`: The function used to run the commander. Commanders require args and sinks to be passed. +function commander.registry() end + return commander diff --git a/emmyLuaDocs/fs.lua b/emmyLuaDocs/fs.lua index 14e7be4..ef80eba 100644 --- a/emmyLuaDocs/fs.lua +++ b/emmyLuaDocs/fs.lua @@ -2,42 +2,53 @@ local fs = {} ---- Gives an absolute version of `path`. ---- @param path string +--- Returns an absolute version of the `path`. +--- This can be used to resolve short paths like `..` to `/home/user`. function fs.abs(path) end ---- Gives the basename of `path`. For the rules, ---- see Go's filepath.Base -function fs.basename() end +--- Returns the "basename," or the last part of the provided `path`. If path is empty, +--- `.` will be returned. +function fs.basename(path) end ---- Changes directory to `dir` ---- @param dir string +--- Changes Hilbish's directory to `dir`. function fs.cd(dir) end ---- Returns the directory part of `path`. For the rules, see Go's ---- filepath.Dir -function fs.dir() end +--- Returns the directory part of `path`. If a file path like +--- `~/Documents/doc.txt` then this function will return `~/Documents`. +function fs.dir(path) end ---- Glob all files and directories that match the pattern. ---- For the rules, see Go's filepath.Glob -function fs.glob() end +--- Match all files based on the provided `pattern`. +--- For the syntax' refer to Go's filepath.Match function: https://pkg.go.dev/path/filepath#Match +--- +--- +function fs.glob(pattern) end ---- Takes paths and joins them together with the OS's ---- directory separator (forward or backward slash). -function fs.join() end +--- Takes any list of paths and joins them based on the operating system's path separator. +--- +--- +function fs.join(...path) end ---- Makes a directory called `name`. If `recursive` is true, it will create its parent directories. ---- @param name string ---- @param recursive boolean +--- Creates a new directory with the provided `name`. +--- With `recursive`, mkdir will create parent directories. +--- +--- function fs.mkdir(name, recursive) end ---- Returns a table of files in `dir` ---- @param dir string ---- @return table -function fs.readdir(dir) end +--- Returns a pair of connected files, also known as a pipe. +--- The type returned is a Lua file, same as returned from `io` functions. +function fs.fpipe() end ---- Returns info about `path` ---- @param path string +--- Returns a list of all files and directories in the provided path. +function fs.readdir(path) end + +--- Returns the information about a given `path`. +--- The returned table contains the following values: +--- name (string) - Name of the path +--- size (number) - Size of the path in bytes +--- mode (string) - Unix permission mode in an octal format string (with leading 0) +--- isDir (boolean) - If the path is a directory +--- +--- function fs.stat(path) end return fs diff --git a/emmyLuaDocs/hilbish.lua b/emmyLuaDocs/hilbish.lua index ca34425..b80a660 100644 --- a/emmyLuaDocs/hilbish.lua +++ b/emmyLuaDocs/hilbish.lua @@ -2,40 +2,92 @@ local hilbish = {} ---- Sets an alias of `cmd` to `orig` +--- This is an alias (ha) for the [hilbish.alias](../#alias) function. +--- @param alias string --- @param cmd string ---- @param orig string +function hilbish.aliases.add(alias, cmd) end + +--- This is the same as the `hilbish.runnerMode` function. +--- It takes a callback, which will be used to execute all interactive input. +--- In normal cases, neither callbacks should be overrided by the user, +--- as the higher level functions listed below this will handle it. +function hilbish.runner.setMode(cb) end + +--- Returns the current input line. +function hilbish.editor.getLine() end + +--- Returns the text that is at the register. +function hilbish.editor.getVimRegister(register) end + +--- Inserts text into the Hilbish command line. +function hilbish.editor.insert(text) end + +--- Reads a keystroke from the user. This is in a format of something like Ctrl-L. +function hilbish.editor.getChar() end + +--- Sets the vim register at `register` to hold the passed text. +function hilbish.editor.setVimRegister(register, text) end + +--- Return binaries/executables based on the provided parameters. +--- This function is meant to be used as a helper in a command completion handler. +--- +--- +function hilbish.completion.bins(query, ctx, fields) end + +--- Calls a completer function. This is mainly used to call a command completer, which will have a `name` +--- in the form of `command.name`, example: `command.git`. +--- You can check the Completions doc or `doc completions` for info on the `completionGroups` return value. +function hilbish.completion.call(name, query, ctx, fields) end + +--- Returns file matches based on the provided parameters. +--- This function is meant to be used as a helper in a command completion handler. +function hilbish.completion.files(query, ctx, fields) end + +--- This function contains the general completion handler for Hilbish. This function handles +--- completion of everything, which includes calling other command handlers, binaries, and files. +--- This function can be overriden to supply a custom handler. Note that alias resolution is required to be done in this function. +--- +--- +function hilbish.completion.handler(line, pos) end + +--- Sets an alias, with a name of `cmd` to another command. +--- +--- function hilbish.alias(cmd, orig) end ---- Appends `dir` to $PATH ---- @param dir string|table +--- Appends the provided dir to the command path (`$PATH`) +--- +--- function hilbish.appendPath(dir) end ---- Registers a completion handler for `scope`. ---- A `scope` is currently only expected to be `command.`, +--- Registers a completion handler for the specified scope. +--- A `scope` is expected to be `command.`, --- replacing with the name of the command (for example `command.git`). ---- `cb` must be a function that returns a table of "completion groups." ---- Check `doc completions` for more information. ---- @param scope string ---- @param cb function +--- The documentation for completions, under Features/Completions or `doc completions` +--- provides more details. +--- +--- function hilbish.complete(scope, cb) end ---- Returns the current directory of the shell +--- Returns the current directory of the shell. function hilbish.cwd() end ---- Replaces running hilbish with `cmd` ---- @param cmd string +--- Replaces the currently running Hilbish instance with the supplied command. +--- This can be used to do an in-place restart. function hilbish.exec(cmd) end ---- Puts `fn` in a goroutine ---- @param fn function +--- Puts `fn` in a Goroutine. +--- This can be used to run any function in another thread at the same time as other Lua code. +--- **NOTE: THIS FUNCTION MAY CRASH HILBISH IF OUTSIDE VARIABLES ARE ACCESSED.** +--- **This is a limitation of the Lua runtime.** function hilbish.goro(fn) end ---- Line highlighter handler. This is mainly for syntax highlighting, but in ---- reality could set the input of the prompt to *display* anything. The ---- callback is passed the current line and is expected to return a line that ---- will be used as the input display. ---- @param line string +--- Line highlighter handler. +--- This is mainly for syntax highlighting, but in reality could set the input +--- of the prompt to *display* anything. The callback is passed the current line +--- and is expected to return a line that will be used as the input display. +--- Note that to set a highlighter, one has to override this function. +--- function hilbish.highlighter(line) end --- The command line hint handler. It gets called on every key insert to @@ -43,68 +95,171 @@ function hilbish.highlighter(line) end --- line and cursor position. It is expected to return a string which is used --- as the text for the hint. This is by default a shim. To set hints, --- override this function with your custom handler. ---- @param line string ---- @param pos int +--- +--- function hilbish.hinter(line, pos) end ---- Sets the input mode for Hilbish's line reader. Accepts either emacs or vim ---- @param mode string +--- Sets the input mode for Hilbish's line reader. +--- `emacs` is the default. Setting it to `vim` changes behavior of input to be +--- Vim-like with modes and Vim keybinds. function hilbish.inputMode(mode) end ---- Runs the `cb` function every `time` milliseconds. ---- Returns a `timer` object (see `doc timers`). ---- @param cb function ---- @param time number ---- @return table +--- Runs the `cb` function every specified amount of `time`. +--- This creates a timer that ticking immediately. function hilbish.interval(cb, time) end ---- Changes the continued line prompt to `str` ---- @param str string +--- Changes the text prompt when Hilbish asks for more input. +--- This will show up when text is incomplete, like a missing quote +--- +--- function hilbish.multiprompt(str) end ---- Prepends `dir` to $PATH ---- @param dir string +--- Prepends `dir` to $PATH. function hilbish.prependPath(dir) end ---- Changes the shell prompt to `str` +--- Changes the shell prompt to the provided string. --- There are a few verbs that can be used in the prompt text. --- These will be formatted and replaced with the appropriate values. --- `%d` - Current working directory --- `%u` - Name of current user --- `%h` - Hostname of device ---- @param str string ---- @param typ string Type of prompt, being left or right. Left by default. +--- function hilbish.prompt(str, typ) end --- Read input from the user, using Hilbish's line editor/input reader. --- This is a separate instance from the one Hilbish actually uses. ---- Returns `input`, will be nil if ctrl + d is pressed, or an error occurs (which shouldn't happen) ---- @param prompt string +--- Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs. function hilbish.read(prompt) end ---- Runs `cmd` in Hilbish's sh interpreter. ---- If returnOut is true, the outputs of `cmd` will be returned as the 2nd and ---- 3rd values instead of being outputted to the terminal. ---- @param cmd string -function hilbish.run(cmd) end +--- Runs `cmd` in Hilbish's shell script interpreter. +--- The `streams` parameter specifies the output and input streams the command should use. +--- For example, to write command output to a sink. +--- As a table, the caller can directly specify the standard output, error, and input +--- streams of the command with the table keys `out`, `err`, and `input` respectively. +--- As a boolean, it specifies whether the command should use standard output or return its output streams. +--- +function hilbish.run(cmd, streams) end ---- Sets the execution/runner mode for interactive Hilbish. This determines whether ---- Hilbish wll try to run input as Lua and/or sh or only do one of either. +--- Sets the execution/runner mode for interactive Hilbish. +--- This determines whether Hilbish wll try to run input as Lua +--- and/or sh or only do one of either. --- Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua), --- sh, and lua. It also accepts a function, to which if it is passed one --- will call it to execute user input instead. ---- @param mode string|function +--- Read [about runner mode](../features/runner-mode) for more information. function hilbish.runnerMode(mode) end ---- Runs the `cb` function after `time` in milliseconds ---- Returns a `timer` object (see `doc timers`). ---- @param cb function ---- @param time number ---- @return table +--- Executed the `cb` function after a period of `time`. +--- This creates a Timer that starts ticking immediately. function hilbish.timeout(cb, time) end ---- Checks if `name` is a valid command ---- @param binName string -function hilbish.which(binName) end +--- Checks if `name` is a valid command. +--- Will return the path of the binary, or a basename if it's a commander. +function hilbish.which(name) end + +--- Puts a job in the background. This acts the same as initially running a job. +function hilbish.jobs:background() end + +--- Puts a job in the foreground. This will cause it to run like it was +--- executed normally and wait for it to complete. +function hilbish.jobs:foreground() end + +--- Evaluates `cmd` as Lua input. This is the same as using `dofile` +--- or `load`, but is appropriated for the runner interface. +function hilbish.runner.lua(cmd) end + +--- Sets/toggles the option of automatically flushing output. +--- A call with no argument will toggle the value. +--- @param auto boolean|nil +function hilbish:autoFlush(auto) end + +--- Flush writes all buffered input to the sink. +function hilbish:flush() end + +--- Reads a liine of input from the sink. +--- @returns string +function hilbish:read() end + +--- Reads all input from the sink. +--- @returns string +function hilbish:readAll() end + +--- Writes data to a sink. +function hilbish:write(str) end + +--- Writes data to a sink with a newline at the end. +function hilbish:writeln(str) end + +--- Starts running the job. +function hilbish.jobs:start() end + +--- Stops the job from running. +function hilbish.jobs:stop() end + +--- Loads a module at the designated `path`. +--- It will throw if any error occurs. +function hilbish.module.load(path) end + +--- Runs a command in Hilbish's shell script interpreter. +--- This is the equivalent of using `source`. +function hilbish.runner.sh(cmd) end + +--- Starts a timer. +function hilbish.timers:start() end + +--- Stops a timer. +function hilbish.timers:stop() end + +--- Removes an alias. +function hilbish.aliases.delete(name) end + +--- Get a table of all aliases, with string keys as the alias and the value as the command. +--- +--- +function hilbish.aliases.list() end + +--- Resolves an alias to its original command. Will thrown an error if the alias doesn't exist. +function hilbish.aliases.resolve(alias) end + +--- Creates a new job. This function does not run the job. This function is intended to be +--- used by runners, but can also be used to create jobs via Lua. Commanders cannot be ran as jobs. +--- +--- +function hilbish.jobs.add(cmdstr, args, execPath) end + +--- Returns a table of all job objects. +function hilbish.jobs.all() end + +--- Disowns a job. This simply deletes it from the list of jobs without stopping it. +function hilbish.jobs.disown(id) end + +--- Get a job object via its ID. +--- @param id number +--- @returns Job +function hilbish.jobs.get(id) end + +--- Returns the last added job to the table. +function hilbish.jobs.last() end + +--- Adds a command to the history. +function hilbish.history.add(cmd) end + +--- Retrieves all history as a table. +function hilbish.history.all() end + +--- Deletes all commands from the history. +function hilbish.history.clear() end + +--- Retrieves a command from the history based on the `index`. +function hilbish.history.get(index) end + +--- Returns the amount of commands in the history. +function hilbish.history.size() end + +--- Creates a timer that runs based on the specified `time`. +function hilbish.timers.create(type, time, callback) end + +--- Retrieves a timer via its ID. +function hilbish.timers.get(id) end return hilbish diff --git a/emmyLuaDocs/terminal.lua b/emmyLuaDocs/terminal.lua index 2266ac6..ed0b9ef 100644 --- a/emmyLuaDocs/terminal.lua +++ b/emmyLuaDocs/terminal.lua @@ -5,14 +5,14 @@ local terminal = {} --- Restores the last saved state of the terminal function terminal.restoreState() end ---- Saves the current state of the terminal +--- Saves the current state of the terminal. function terminal.saveState() end ---- Puts the terminal in raw mode +--- Puts the terminal into raw mode. function terminal.setRaw() end --- Gets the dimensions of the terminal. Returns a table with `width` and `height` ---- Note: this is not the size in relation to the dimensions of the display +--- NOTE: The size refers to the amount of columns and rows of text that can fit in the terminal. function terminal.size() end return terminal diff --git a/exec.go b/exec.go index 1d4b07d..77b72ec 100644 --- a/exec.go +++ b/exec.go @@ -29,6 +29,12 @@ var errNotExec = errors.New("not executable") var errNotFound = errors.New("not found") var runnerMode rt.Value = rt.StringValue("hybrid") +type streams struct { + stdout io.Writer + stderr io.Writer + stdin io.Reader +} + type execError struct{ typ string cmd string @@ -97,23 +103,23 @@ func runInput(input string, priv bool) { if currentRunner.Type() == rt.StringType { switch currentRunner.AsString() { case "hybrid": - _, _, err = handleLua(cmdString) + _, _, err = handleLua(input) if err == nil { cmdFinish(0, input, priv) return } - input, exitCode, cont, err = handleSh(cmdString) + input, exitCode, cont, err = handleSh(input) case "hybridRev": _, _, _, err = handleSh(input) if err == nil { cmdFinish(0, input, priv) return } - input, exitCode, err = handleLua(cmdString) + input, exitCode, err = handleLua(input) case "lua": - input, exitCode, err = handleLua(cmdString) + input, exitCode, err = handleLua(input) case "sh": - input, exitCode, cont, err = handleSh(cmdString) + input, exitCode, cont, err = handleSh(input) } } else { // can only be a string or function so @@ -142,9 +148,9 @@ func runInput(input string, priv bool) { if err != nil { if exErr, ok := isExecError(err); ok { hooks.Emit("command." + exErr.typ, exErr.cmd) - err = exErr.sprint() + } else { + fmt.Fprintln(os.Stderr, err) } - fmt.Fprintln(os.Stderr, err) } cmdFinish(exitCode, input, priv) } @@ -176,6 +182,9 @@ func runLuaRunner(runr rt.Value, userInput string) (input string, exitCode uint8 runnerRet := term.Get(0) if runner, ok = runnerRet.TryTable(); !ok { fmt.Fprintln(os.Stderr, "runner did not return a table") + exitCode = 125 + input = userInput + return } if code, ok := runner.Get(rt.StringValue("exitCode")).TryInt(); ok { @@ -196,7 +205,8 @@ func runLuaRunner(runr rt.Value, userInput string) (input string, exitCode uint8 return } -func handleLua(cmdString string) (string, uint8, error) { +func handleLua(input string) (string, uint8, error) { + cmdString := aliases.Resolve(input) // First try to load input, essentially compiling to bytecode chunk, err := l.CompileAndLoadLuaChunk("", []byte(cmdString), rt.TableValue(l.GlobalEnv())) if err != nil && noexecute { @@ -233,7 +243,7 @@ func handleSh(cmdString string) (input string, exitCode uint8, cont bool, runErr } func execSh(cmdString string) (string, uint8, bool, error) { - _, _, err := execCommand(cmdString, true) + _, _, err := execCommand(cmdString, nil) if err != nil { // If input is incomplete, start multiline prompting if syntax.IsIncomplete(err) { @@ -254,7 +264,7 @@ func execSh(cmdString string) (string, uint8, bool, error) { } // Run command in sh interpreter -func execCommand(cmd string, terminalOut bool) (io.Writer, io.Writer, error) { +func execCommand(cmd string, strms *streams) (io.Writer, io.Writer, error) { file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "") if err != nil { return nil, nil, err @@ -262,15 +272,24 @@ func execCommand(cmd string, terminalOut bool) (io.Writer, io.Writer, error) { runner, _ := interp.New() - var stdout io.Writer - var stderr io.Writer - if terminalOut { - interp.StdIO(os.Stdin, os.Stdout, os.Stderr)(runner) - } else { - stdout = new(bytes.Buffer) - stderr = new(bytes.Buffer) - interp.StdIO(os.Stdin, stdout, stderr)(runner) + if strms == nil { + strms = &streams{} } + + if strms.stdout == nil { + strms.stdout = os.Stdout + } + + if strms.stderr == nil { + strms.stderr = os.Stderr + } + + if strms.stdin == nil { + strms.stdin = os.Stdin + } + + interp.StdIO(strms.stdin, strms.stdout, strms.stderr)(runner) + buf := new(bytes.Buffer) printer := syntax.NewPrinter() @@ -289,11 +308,11 @@ func execCommand(cmd string, terminalOut bool) (io.Writer, io.Writer, error) { interp.ExecHandler(execHandle(bg))(runner) err = runner.Run(context.TODO(), stmt) if err != nil { - return stdout, stderr, err + return strms.stdout, strms.stderr, err } } - return stdout, stderr, nil + return strms.stdout, strms.stderr, nil } func execHandle(bg bool) interp.ExecHandlerFunc { @@ -323,30 +342,19 @@ func execHandle(bg bool) interp.ExecHandlerFunc { luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str)) } - if commands[args[0]] != nil { - t := rt.NewThread(l) - sig := make(chan os.Signal) - exit := make(chan bool) + hc := interp.HandlerCtx(ctx) + if cmd := cmds.Commands[args[0]]; cmd != nil { + stdin := newSinkInput(hc.Stdin) + stdout := newSinkOutput(hc.Stdout) + stderr := newSinkOutput(hc.Stderr) - go func() { - defer func() { - if r := recover(); r != nil { - fmt.Println(r) - } - }() + sinks := rt.NewTable() + sinks.Set(rt.StringValue("in"), rt.UserDataValue(stdin.ud)) + sinks.Set(rt.StringValue("input"), rt.UserDataValue(stdin.ud)) + sinks.Set(rt.StringValue("out"), rt.UserDataValue(stdout.ud)) + sinks.Set(rt.StringValue("err"), rt.UserDataValue(stderr.ud)) - signal.Notify(sig, os.Interrupt) - select { - case <-sig: - t.KillContext() - return - case <-exit: - return - } - }() - - luaexitcode, err := rt.Call1(t, rt.FunctionValue(commands[args[0]]), rt.TableValue(luacmdArgs)) - exit <- true + luaexitcode, err := rt.Call1(l.MainThread(), rt.FunctionValue(cmd), rt.TableValue(luacmdArgs), rt.TableValue(sinks)) if err != nil { fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error()) return interp.NewExitStatus(1) @@ -358,7 +366,7 @@ func execHandle(bg bool) interp.ExecHandlerFunc { exitcode = uint8(code) } else if luaexitcode != rt.NilValue { // deregister commander - delete(commands, args[0]) + delete(cmds.Commands, args[0]) fmt.Fprintf(os.Stderr, "Commander did not return number for exit code. %s, you're fired.\n", args[0]) } @@ -386,7 +394,6 @@ func execHandle(bg bool) interp.ExecHandlerFunc { killTimeout := 2 * time.Second // from here is basically copy-paste of the default exec handler from // sh/interp but with our job handling - hc := interp.HandlerCtx(ctx) path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0]) if err != nil { fmt.Fprintln(hc.Stderr, err) @@ -574,7 +581,7 @@ func splitInput(input string) ([]string, string) { } func cmdFinish(code uint8, cmdstr string, private bool) { - util.SetField(l, hshMod, "exitCode", rt.IntValue(int64(code)), "Exit code of last exected command") + util.SetField(l, hshMod, "exitCode", rt.IntValue(int64(code))) // using AsValue (to convert to lua type) on an interface which is an int // results in it being unknown in lua .... ???? // so we allow the hook handler to take lua runtime Values diff --git a/execfile_unix.go b/execfile_unix.go index 44f924a..82c738b 100644 --- a/execfile_unix.go +++ b/execfile_unix.go @@ -1,4 +1,4 @@ -// +build linux darwin +//go:build unix package main diff --git a/execfile_windows.go b/execfile_windows.go index 4b3feef..3d6ef61 100644 --- a/execfile_windows.go +++ b/execfile_windows.go @@ -1,4 +1,4 @@ -// +build windows +//go:build windows package main diff --git a/gallery/tab.png b/gallery/tab.png new file mode 100644 index 0000000..409d796 Binary files /dev/null and b/gallery/tab.png differ diff --git a/go.mod b/go.mod index 825dae0..a7975b7 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,28 @@ module hilbish -go 1.17 +go 1.18 require ( - github.com/arnodel/golua v0.0.0-20220221163911-dfcf252b6f86 - github.com/blackfireio/osinfo v1.0.3 - github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 - github.com/maxlandon/readline v0.1.0-beta.0.20211027085530-2b76cabb8036 + github.com/arnodel/golua v0.0.0-20230215163904-e0b5347eaaa1 + github.com/atsushinee/go-markdown-generator v0.0.0-20191121114853-83f9e1f68504 + github.com/blackfireio/osinfo v1.0.5 + github.com/maxlandon/readline v1.0.14 github.com/pborman/getopt v1.1.0 - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a - golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 - mvdan.cc/sh/v3 v3.5.1 + github.com/sahilm/fuzzy v0.1.1 + golang.org/x/sys v0.19.0 + golang.org/x/term v0.19.0 + mvdan.cc/sh/v3 v3.8.0 ) require ( github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/arnodel/strftime v0.1.6 // indirect - github.com/evilsocket/islazy v1.10.6 // indirect + github.com/evilsocket/islazy v1.11.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 // indirect - github.com/rivo/uniseg v0.2.0 // indirect - golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect - golang.org/x/text v0.3.7 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/text v0.14.0 // indirect ) replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220524215627-dfd9a4fa219b @@ -29,4 +31,4 @@ replace github.com/maxlandon/readline => ./readline replace layeh.com/gopher-luar => github.com/layeh/gopher-luar v1.0.10 -replace github.com/arnodel/golua => github.com/Rosettea/golua v0.0.0-20220518005949-116371948fe3 +replace github.com/arnodel/golua => github.com/Rosettea/golua v0.0.0-20240427174124-d239074c1749 diff --git a/go.sum b/go.sum index c313c19..193f17e 100644 --- a/go.sum +++ b/go.sum @@ -1,30 +1,21 @@ -github.com/Rosettea/golua v0.0.0-20220419183026-6d22d6fec5ac h1:dtXrgjch8PQyf7C90anZUquB5U3dr8AcMGJofeuirrI= -github.com/Rosettea/golua v0.0.0-20220419183026-6d22d6fec5ac/go.mod h1:9jzpYPiU2is0HVGCiuIOBSXdergHUW44IEjmuN1UrIE= -github.com/Rosettea/golua v0.0.0-20220518005949-116371948fe3 h1:I/wWr40FFLFF9pbT3wLb1FAEZhKb/hUWE+nJ5uHBK2g= -github.com/Rosettea/golua v0.0.0-20220518005949-116371948fe3/go.mod h1:9jzpYPiU2is0HVGCiuIOBSXdergHUW44IEjmuN1UrIE= -github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e h1:P2XupP8SaylWaudD1DqbWtZ3mIa8OsE9635LmR+Q+lg= -github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e/go.mod h1:R09vh/04ILvP2Gj8/Z9Jd0Dh0ZIvaucowMEs6abQpWs= +github.com/Rosettea/golua v0.0.0-20240427174124-d239074c1749 h1:jIFnWBTsYw8s7RX7H2AOXjDVhWP3ol7OzUVaPN2KnGI= +github.com/Rosettea/golua v0.0.0-20240427174124-d239074c1749/go.mod h1:9jzpYPiU2is0HVGCiuIOBSXdergHUW44IEjmuN1UrIE= github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220524215627-dfd9a4fa219b h1:s5eDMhBk6H1BgipgLub/gv9qeyBaTuiHM0k3h2/9TSE= github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220524215627-dfd9a4fa219b/go.mod h1:R09vh/04ILvP2Gj8/Z9Jd0Dh0ZIvaucowMEs6abQpWs= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/arnodel/edit v0.0.0-20220202110212-dfc8d7a13890/go.mod h1:AcpttpuZBaL9xl8/CX+Em4fBTUbwIkJ66RiAsJlNrBk= github.com/arnodel/strftime v0.1.6 h1:0hc0pUvk8KhEMXE+htyaOUV42zNcf/csIbjzEFCJqsw= github.com/arnodel/strftime v0.1.6/go.mod h1:5NbK5XqYK8QpRZpqKNt4OlxLtIB8cotkLk4KTKzJfWs= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/blackfireio/osinfo v1.0.3 h1:Yk2t2GTPjBcESv6nDSWZKO87bGMQgO+Hi9OoXPpxX8c= -github.com/blackfireio/osinfo v1.0.3/go.mod h1:Pd987poVNmd5Wsx6PRPw4+w7kLlf9iJxoRKPtPAjOrA= -github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 h1:xz6Nv3zcwO2Lila35hcb0QloCQsc38Al13RNEzWRpX4= -github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9/go.mod h1:2wSM9zJkl1UQEFZgSd68NfCgRz1VL1jzy/RjCg+ULrs= +github.com/atsushinee/go-markdown-generator v0.0.0-20191121114853-83f9e1f68504 h1:R1/AOzdMbopSliUTTEHvHbyNmnZ3YxY5GvdhTkpPsSY= +github.com/atsushinee/go-markdown-generator v0.0.0-20191121114853-83f9e1f68504/go.mod h1:kHBCvAXJIatTX1pw6tLiOspjGc3MhUDRlog9yrCUS+k= +github.com/blackfireio/osinfo v1.0.5 h1:6hlaWzfcpb87gRmznVf7wSdhysGqLRz9V/xuSdCEXrA= +github.com/blackfireio/osinfo v1.0.5/go.mod h1:Pd987poVNmd5Wsx6PRPw4+w7kLlf9iJxoRKPtPAjOrA= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc= github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/evilsocket/islazy v1.10.6 h1:MFq000a1ByoumoJWlytqg0qon0KlBeUfPsDjY0hK0bo= -github.com/evilsocket/islazy v1.10.6/go.mod h1:OrwQGYg3DuZvXUfmH+KIZDjwTCbrjy48T24TUpGqVVw= -github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= -github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= +github.com/evilsocket/islazy v1.11.0 h1:B5w6uuS6ki6iDG+aH/RFeoMb8ijQh/pGabewqp2UeJ0= +github.com/evilsocket/islazy v1.11.0/go.mod h1:muYH4x5MB5YRdkxnrOtrXLIBX6LySj1uFIqys94LKdo= github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= -github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= @@ -32,44 +23,32 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 h1:LiZB1h0GIcudcDci2bxbqI6DXV8bF8POAnArqvRrIyw= github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= github.com/pborman/getopt v1.1.0 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0= github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451 h1:d1PiN4RxzIFXCJTvRkvSkKqwtRAl5ZV4lATKtQI0B7I= github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= -golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210925032602-92d5a993a665/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= -golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= mvdan.cc/editorconfig v0.2.0/go.mod h1:lvnnD3BNdBYkhq+B4uBuFFKatfp02eB6HixDvEz91C0= diff --git a/golibs/bait/bait.go b/golibs/bait/bait.go index 89e0c4a..1f85c76 100644 --- a/golibs/bait/bait.go +++ b/golibs/bait/bait.go @@ -1,6 +1,33 @@ +// the event emitter +/* +Bait is the event emitter for Hilbish. Much like Node.js and +its `events` system, many actions in Hilbish emit events. +Unlike Node.js, Hilbish events are global. So make sure to +pick a unique name! + +Usage of the Bait module consists of userstanding +event-driven architecture, but it's pretty simple: +If you want to act on a certain event, you can `catch` it. +You can act on events via callback functions. + +Examples of this are in the Hilbish default config! +Consider this part of it: +```lua +bait.catch('command.exit', function(code) + running = false + doPrompt(code ~= 0) + doNotifyPrompt() +end) +``` + +What this does is, whenever the `command.exit` event is thrown, +this function will set the user prompt. +*/ package bait import ( + "errors" + "hilbish/util" rt "github.com/arnodel/golua/runtime" @@ -72,8 +99,12 @@ func (b *Bait) Emit(event string, args ...interface{}) { } _, err := rt.Call1(b.rtm.MainThread(), funcVal, luaArgs...) if err != nil { - // panicking here won't actually cause hilbish to panic and instead will - // print the error and remove the hook. reference the recoverer function in lua.go + if event != "error" { + b.Emit("error", event, handle.luaCaller, err.Error()) + return + } + // if there is an error in an error event handler, panic instead + // (calls the go recoverer function) panic(err) } } else { @@ -187,19 +218,11 @@ func (b *Bait) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { "catchOnce": util.LuaExport{b.bcatchOnce, 2, false}, "throw": util.LuaExport{b.bthrow, 1, true}, "release": util.LuaExport{b.brelease, 2, false}, + "hooks": util.LuaExport{b.bhooks, 1, false}, } mod := rt.NewTable() util.SetExports(rtm, mod, exports) - util.Document(mod, -`Bait is the event emitter for Hilbish. Why name it bait? -Because it throws hooks that you can catch (emits events -that you can listen to) and because why not, fun naming -is fun. This is what you will use if you want to listen -in on hooks to know when certain things have happened, -like when you've changed directory, a command has -failed, etc. To find all available hooks, see doc hooks.`) - return rt.TableValue(mod), nil } @@ -224,10 +247,117 @@ func handleHook(t *rt.Thread, c *rt.GoCont, name string, catcher *rt.Closure, ar } } +// catch(name, cb) +// Catches an event. This function can be used to act on events. +// #param name string The name of the hook. +// #param cb function The function that will be called when the hook is thrown. +/* +#example +bait.catch('hilbish.exit', function() + print 'Goodbye Hilbish!' +end) +#example +*/ +func (b *Bait) bcatch(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + name, catcher, err := util.HandleStrCallback(t, c) + if err != nil { + return nil, err + } + + b.OnLua(name, catcher) + + return c.Next(), nil +} + +// catchOnce(name, cb) +// Catches an event, but only once. This will remove the hook immediately after it runs for the first time. +// #param name string The name of the event +// #param cb function The function that will be called when the event is thrown. +func (b *Bait) bcatchOnce(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + name, catcher, err := util.HandleStrCallback(t, c) + if err != nil { + return nil, err + } + + b.OnceLua(name, catcher) + + return c.Next(), nil +} + +// hooks(name) -> table +// Returns a table of functions that are hooked on an event with the corresponding `name`. +// #param name string The name of the hook +// #returns table +func (b *Bait) bhooks(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + evName, err := c.StringArg(0) + if err != nil { + return nil, err + } + noHooks := errors.New("no hooks for event " + evName) + + handlers := b.handlers[evName] + if handlers == nil { + return nil, noHooks + } + + luaHandlers := rt.NewTable() + for _, handler := range handlers { + if handler.typ != luaListener { continue } + luaHandlers.Set(rt.IntValue(luaHandlers.Len() + 1), rt.FunctionValue(handler.luaCaller)) + } + + if luaHandlers.Len() == 0 { + return nil, noHooks + } + + return c.PushingNext1(t.Runtime, rt.TableValue(luaHandlers)), nil +} + +// release(name, catcher) +// Removes the `catcher` for the event with `name`. +// For this to work, `catcher` has to be the same function used to catch +// an event, like one saved to a variable. +// #param name string Name of the event the hook is on +// #param catcher function Hook function to remove +/* +#example +local hookCallback = function() print 'hi' end + +bait.catch('event', hookCallback) + +-- a little while later.... +bait.release('event', hookCallback) +-- and now hookCallback will no longer be ran for the event. +#example +*/ +func (b *Bait) brelease(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + name, catcher, err := util.HandleStrCallback(t, c) + if err != nil { + return nil, err + } + + b.OffLua(name, catcher) + + return c.Next(), nil +} + // throw(name, ...args) -// Throws a hook with `name` with the provided `args` -// --- @param name string -// --- @vararg any +// #param name string The name of the hook. +// #param args ...any The arguments to pass to the hook. +// Throws a hook with `name` with the provided `args`. +/* +#example +bait.throw('greeting', 'world') + +-- This can then be listened to via +bait.catch('gretting', function(greetTo) + print('Hello ' .. greetTo) +end) +#example +*/ func (b *Bait) bthrow(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -244,48 +374,3 @@ func (b *Bait) bthrow(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } - -// catch(name, cb) -// Catches a hook with `name`. Runs the `cb` when it is thrown -// --- @param name string -// --- @param cb function -func (b *Bait) bcatch(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - name, catcher, err := util.HandleStrCallback(t, c) - if err != nil { - return nil, err - } - - b.OnLua(name, catcher) - - return c.Next(), nil -} - -// catchOnce(name, cb) -// Same as catch, but only runs the `cb` once and then removes the hook -// --- @param name string -// --- @param cb function -func (b *Bait) bcatchOnce(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - name, catcher, err := util.HandleStrCallback(t, c) - if err != nil { - return nil, err - } - - b.OnceLua(name, catcher) - - return c.Next(), nil -} - -// release(name, catcher) -// Removes the `catcher` for the event with `name` -// For this to work, `catcher` has to be the same function used to catch -// an event, like one saved to a variable. -func (b *Bait) brelease(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - name, catcher, err := util.HandleStrCallback(t, c) - if err != nil { - return nil, err - } - - b.OffLua(name, catcher) - - return c.Next(), nil -} diff --git a/golibs/commander/commander.go b/golibs/commander/commander.go index 24f1c03..840aaa1 100644 --- a/golibs/commander/commander.go +++ b/golibs/commander/commander.go @@ -1,3 +1,35 @@ +// library for custom commands +/* +Commander is the library which handles Hilbish commands. This makes +the user able to add Lua-written commands to their shell without making +a separate script in a bin folder. Instead, you may simply use the Commander +library in your Hilbish config. + +```lua +local commander = require 'commander' + +commander.register('hello', function(args, sinks) + sinks.out:writeln 'Hello world!' +end) +``` + +In this example, a command with the name of `hello` is created +that will print `Hello world!` to output. One question you may +have is: What is the `sinks` parameter? + +The `sinks` parameter is a table with 3 keys: `input`, `out`, and `err`. +There is an `in` alias to `input`, but it requires using the string accessor syntax (`sinks['in']`) +as `in` is also a Lua keyword, so `input` is preferred for use. +All of them are a @Sink. +In the future, `sinks.in` will be removed. + +- `in` is the standard input. +You may use the read functions on this sink to get input from the user. +- `out` is standard output. +This is usually where command output should go. +- `err` is standard error. +This sink is for writing errors, as the name would suggest. +*/ package commander import ( @@ -11,11 +43,13 @@ import ( type Commander struct{ Events *bait.Bait Loader packagelib.Loader + Commands map[string]*rt.Closure } -func New(rtm *rt.Runtime) Commander { - c := Commander{ +func New(rtm *rt.Runtime) *Commander { + c := &Commander{ Events: bait.New(rtm), + Commands: make(map[string]*rt.Closure), } c.Loader = packagelib.Loader{ Load: c.loaderFunc, @@ -29,32 +63,45 @@ func (c *Commander) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { exports := map[string]util.LuaExport{ "register": util.LuaExport{c.cregister, 2, false}, "deregister": util.LuaExport{c.cderegister, 1, false}, + "registry": util.LuaExport{c.cregistry, 0, false}, } mod := rt.NewTable() util.SetExports(rtm, mod, exports) - util.Document(mod, "Commander is Hilbish's custom command library, a way to write commands in Lua.") return rt.TableValue(mod), nil } // register(name, cb) -// Register a command with `name` that runs `cb` when ran -// --- @param name string -// --- @param cb function +// Adds a new command with the given `name`. When Hilbish has to run a command with a name, +// it will run the function providing the arguments and sinks. +// #param name string Name of the command +// #param cb function Callback to handle command invocation +/* +#example +-- When you run the command `hello` in the shell, it will print `Hello world`. +-- If you run it with, for example, `hello Hilbish`, it will print 'Hello Hilbish' +commander.register('hello', function(args, sinks) + local name = 'world' + if #args > 0 then name = args[1] end + + sinks.out:writeln('Hello ' .. name) +end) +#example +*/ func (c *Commander) cregister(t *rt.Thread, ct *rt.GoCont) (rt.Cont, error) { cmdName, cmd, err := util.HandleStrCallback(t, ct) if err != nil { return nil, err } - c.Events.Emit("commandRegister", cmdName, cmd) + c.Commands[cmdName] = cmd return ct.Next(), err } // deregister(name) -// Deregisters any command registered with `name` -// --- @param name string +// Removes the named command. Note that this will only remove Commander-registered commands. +// #param name string Name of the command to remove. func (c *Commander) cderegister(t *rt.Thread, ct *rt.GoCont) (rt.Cont, error) { if err := ct.Check1Arg(); err != nil { return nil, err @@ -64,7 +111,23 @@ func (c *Commander) cderegister(t *rt.Thread, ct *rt.GoCont) (rt.Cont, error) { return nil, err } - c.Events.Emit("commandDeregister", cmdName) + delete(c.Commands, cmdName) return ct.Next(), err } + +// registry() -> table +// Returns all registered commanders. Returns a list of tables with the following keys: +// - `exec`: The function used to run the commander. Commanders require args and sinks to be passed. +// #returns table +func (c *Commander) cregistry(t *rt.Thread, ct *rt.GoCont) (rt.Cont, error) { + registryLua := rt.NewTable() + for cmdName, cmd := range c.Commands { + cmdTbl := rt.NewTable() + cmdTbl.Set(rt.StringValue("exec"), rt.FunctionValue(cmd)) + + registryLua.Set(rt.StringValue(cmdName), rt.TableValue(cmdTbl)) + } + + return ct.PushingNext1(t.Runtime, rt.TableValue(registryLua)), nil +} diff --git a/golibs/fs/fs.go b/golibs/fs/fs.go index 5b12e73..9e03325 100644 --- a/golibs/fs/fs.go +++ b/golibs/fs/fs.go @@ -1,3 +1,10 @@ +// filesystem interaction and functionality library +/* +The fs module provides filesystem functions to Hilbish. While Lua's standard +library has some I/O functions, they're missing a lot of the basics. The `fs` +library offers more functions and will work on any operating system Hilbish does. +#field pathSep The operating system's path separator. +*/ package fs import ( @@ -11,6 +18,7 @@ import ( rt "github.com/arnodel/golua/runtime" "github.com/arnodel/golua/lib/packagelib" + "github.com/arnodel/golua/lib/iolib" ) var Loader = packagelib.Loader{ @@ -29,22 +37,56 @@ func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { "dir": util.LuaExport{fdir, 1, false}, "glob": util.LuaExport{fglob, 1, false}, "join": util.LuaExport{fjoin, 0, true}, + "pipe": util.LuaExport{fpipe, 0, false}, } mod := rt.NewTable() util.SetExports(rtm, mod, exports) mod.Set(rt.StringValue("pathSep"), rt.StringValue(string(os.PathSeparator))) mod.Set(rt.StringValue("pathListSep"), rt.StringValue(string(os.PathListSeparator))) - util.Document(mod, `The fs module provides easy and simple access to -filesystem functions and other things, and acts an -addition to the Lua standard library's I/O and filesystem functions.`) - return rt.TableValue(mod), nil } +// abs(path) -> string +// Returns an absolute version of the `path`. +// This can be used to resolve short paths like `..` to `/home/user`. +// #param path string +// #returns string +func fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + path, err := c.StringArg(0) + if err != nil { + return nil, err + } + path = util.ExpandHome(path) + + abspath, err := filepath.Abs(path) + if err != nil { + return nil, err + } + + return c.PushingNext1(t.Runtime, rt.StringValue(abspath)), nil +} + +// basename(path) -> string +// Returns the "basename," or the last part of the provided `path`. If path is empty, +// `.` will be returned. +// #param path string Path to get the base name of. +// #returns string +func fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + path, err := c.StringArg(0) + if err != nil { + return nil, err + } + + return c.PushingNext(t.Runtime, rt.StringValue(filepath.Base(path))), nil +} + // cd(dir) -// Changes directory to `dir` -// --- @param dir string +// Changes Hilbish's directory to `dir`. +// #param dir string Path to change directory to. func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -63,10 +105,103 @@ func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), err } +// dir(path) -> string +// Returns the directory part of `path`. If a file path like +// `~/Documents/doc.txt` then this function will return `~/Documents`. +// #param path string Path to get the directory for. +// #returns string +func fdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + path, err := c.StringArg(0) + if err != nil { + return nil, err + } + + return c.PushingNext(t.Runtime, rt.StringValue(filepath.Dir(path))), nil +} + +// glob(pattern) -> matches (table) +// Match all files based on the provided `pattern`. +// For the syntax' refer to Go's filepath.Match function: https://pkg.go.dev/path/filepath#Match +// #param pattern string Pattern to compare files with. +// #returns table A list of file names/paths that match. +/* +#example +--[[ + Within a folder that contains the following files: + a.txt + init.lua + code.lua + doc.pdf +]]-- +local matches = fs.glob './*.lua' +print(matches) +-- -> {'init.lua', 'code.lua'} +#example +*/ +func fglob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + pattern, err := c.StringArg(0) + if err != nil { + return nil, err + } + + matches, err := filepath.Glob(pattern) + if err != nil { + return nil, err + } + + luaMatches := rt.NewTable() + + for i, match := range matches { + luaMatches.Set(rt.IntValue(int64(i + 1)), rt.StringValue(match)) + } + + return c.PushingNext(t.Runtime, rt.TableValue(luaMatches)), nil +} + +// join(...path) -> string +// Takes any list of paths and joins them based on the operating system's path separator. +// #param path ...string Paths to join together +// #returns string The joined path. +/* +#example +-- This prints the directory for Hilbish's config! +print(fs.join(hilbish.userDir.config, 'hilbish')) +-- -> '/home/user/.config/hilbish' on Linux +#example +*/ +func fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + strs := make([]string, len(c.Etc())) + for i, v := range c.Etc() { + if v.Type() != rt.StringType { + // +2; go indexes of 0 and first arg from above + return nil, fmt.Errorf("bad argument #%d to run (expected string, got %s)", i + 1, v.TypeName()) + } + strs[i] = v.AsString() + } + + res := filepath.Join(strs...) + + return c.PushingNext(t.Runtime, rt.StringValue(res)), nil +} + // mkdir(name, recursive) -// Makes a directory called `name`. If `recursive` is true, it will create its parent directories. -// --- @param name string -// --- @param recursive boolean +// Creates a new directory with the provided `name`. +// With `recursive`, mkdir will create parent directories. +// #param name string Name of the directory +// #param recursive boolean Whether to create parent directories for the provided name +/* +#example +-- This will create the directory foo, then create the directory bar in the +-- foo directory. If recursive is false in this case, it will fail. +fs.mkdir('./foo/bar', true) +#example +*/ func fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.CheckNArgs(2); err != nil { return nil, err @@ -93,9 +228,74 @@ func fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), err } -// stat(path) -// Returns info about `path` -// --- @param path string +// fpipe() -> File, File +// Returns a pair of connected files, also known as a pipe. +// The type returned is a Lua file, same as returned from `io` functions. +// #returns File +// #returns File +func fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + rf, wf, err := os.Pipe() + if err != nil { + return nil, err + } + + rfLua := iolib.NewFile(rf, 0) + wfLua := iolib.NewFile(wf, 0) + + return c.PushingNext(t.Runtime, rfLua.Value(t.Runtime), wfLua.Value(t.Runtime)), nil +} +// readdir(path) -> table[string] +// Returns a list of all files and directories in the provided path. +// #param dir string +// #returns table +func freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + dir, err := c.StringArg(0) + if err != nil { + return nil, err + } + dir = util.ExpandHome(dir) + names := rt.NewTable() + + dirEntries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + for i, entry := range dirEntries { + names.Set(rt.IntValue(int64(i + 1)), rt.StringValue(entry.Name())) + } + + return c.PushingNext1(t.Runtime, rt.TableValue(names)), nil +} + +// stat(path) -> {} +// Returns the information about a given `path`. +// The returned table contains the following values: +// name (string) - Name of the path +// size (number) - Size of the path in bytes +// mode (string) - Unix permission mode in an octal format string (with leading 0) +// isDir (boolean) - If the path is a directory +// #param path string +// #returns table +/* +#example +local inspect = require 'inspect' + +local stat = fs.stat '~' +print(inspect(stat)) +--[[ +Would print the following: +{ + isDir = true, + mode = "0755", + name = "username", + size = 12288 +} +]]-- +#example +*/ func fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -119,120 +319,3 @@ func fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext1(t.Runtime, rt.TableValue(statTbl)), nil } -// readdir(dir) -// Returns a table of files in `dir` -// --- @param dir string -// --- @return table -func freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - if err := c.Check1Arg(); err != nil { - return nil, err - } - dir, err := c.StringArg(0) - if err != nil { - return nil, err - } - dir = util.ExpandHome(dir) - names := rt.NewTable() - - dirEntries, err := os.ReadDir(dir) - if err != nil { - return nil, err - } - for i, entry := range dirEntries { - names.Set(rt.IntValue(int64(i + 1)), rt.StringValue(entry.Name())) - } - - return c.PushingNext1(t.Runtime, rt.TableValue(names)), nil -} - -// abs(path) -// Gives an absolute version of `path`. -// --- @param path string -func fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - path, err := c.StringArg(0) - if err != nil { - return nil, err - } - path = util.ExpandHome(path) - - abspath, err := filepath.Abs(path) - if err != nil { - return nil, err - } - - return c.PushingNext1(t.Runtime, rt.StringValue(abspath)), nil -} - -// basename(path) -// Gives the basename of `path`. For the rules, -// see Go's filepath.Base -func fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - if err := c.Check1Arg(); err != nil { - return nil, err - } - path, err := c.StringArg(0) - if err != nil { - return nil, err - } - - return c.PushingNext(t.Runtime, rt.StringValue(filepath.Base(path))), nil -} - -// dir(path) -// Returns the directory part of `path`. For the rules, see Go's -// filepath.Dir -func fdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - if err := c.Check1Arg(); err != nil { - return nil, err - } - path, err := c.StringArg(0) - if err != nil { - return nil, err - } - - return c.PushingNext(t.Runtime, rt.StringValue(filepath.Dir(path))), nil -} - -// glob(pattern) -// Glob all files and directories that match the pattern. -// For the rules, see Go's filepath.Glob -func fglob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - if err := c.Check1Arg(); err != nil { - return nil, err - } - pattern, err := c.StringArg(0) - if err != nil { - return nil, err - } - - matches, err := filepath.Glob(pattern) - if err != nil { - return nil, err - } - - luaMatches := rt.NewTable() - - for i, match := range matches { - luaMatches.Set(rt.IntValue(int64(i + 1)), rt.StringValue(match)) - } - - return c.PushingNext(t.Runtime, rt.TableValue(luaMatches)), nil -} - -// join(paths...) -// Takes paths and joins them together with the OS's -// directory separator (forward or backward slash). -func fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - strs := make([]string, len(c.Etc())) - for i, v := range c.Etc() { - if v.Type() != rt.StringType { - // +2; go indexes of 0 and first arg from above - return nil, fmt.Errorf("bad argument #%d to run (expected string, got %s)", i + 1, v.TypeName()) - } - strs[i] = v.AsString() - } - - res := filepath.Join(strs...) - - return c.PushingNext(t.Runtime, rt.StringValue(res)), nil -} diff --git a/golibs/terminal/terminal.go b/golibs/terminal/terminal.go index df1755c..954a4dd 100644 --- a/golibs/terminal/terminal.go +++ b/golibs/terminal/terminal.go @@ -1,3 +1,5 @@ +// low level terminal library +// The terminal library is a simple and lower level library for certain terminal interactions. package terminal import ( @@ -26,14 +28,13 @@ func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { mod := rt.NewTable() util.SetExports(rtm, mod, exports) - util.Document(mod, "The terminal library is a simple and lower level library for certain terminal interactions.") return rt.TableValue(mod), nil } // size() // Gets the dimensions of the terminal. Returns a table with `width` and `height` -// Note: this is not the size in relation to the dimensions of the display +// NOTE: The size refers to the amount of columns and rows of text that can fit in the terminal. func termsize(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { w, h, err := term.GetSize(int(os.Stdin.Fd())) if err != nil { @@ -48,7 +49,7 @@ func termsize(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } // saveState() -// Saves the current state of the terminal +// Saves the current state of the terminal. func termsaveState(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { state, err := term.GetState(int(os.Stdin.Fd())) if err != nil { @@ -71,7 +72,7 @@ func termrestoreState(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } // setRaw() -// Puts the terminal in raw mode +// Puts the terminal into raw mode. func termsetRaw(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { _, err := term.MakeRaw(int(os.Stdin.Fd())) if err != nil { diff --git a/hilbish-git.spec b/hilbish-git.spec new file mode 100644 index 0000000..77d2e13 --- /dev/null +++ b/hilbish-git.spec @@ -0,0 +1,59 @@ +%global _missing_build_ids_terminate_build 0 +%global debug_package %{nil} + +Name: hilbish-git +Version: {{{ git_tag_version }}}.{{{ git_short_hash }}} +Release: 1%{?dist} +Summary: The flower shell. A comfy and nice little shell for Lua fans! +License: MIT + +Source: {{{ git_dir_pack }}} +BuildRequires: git golang go-task +Requires: inspect succulent lunacolors + +Url: https://github.com/Rosettea/Hilbish +VCS: {{{ git_dir_vcs }}} + +%description +Hilbish is a extensible shell (framework). It was made to be very customizable +via the Lua programming language. It aims to be easy to use for the casual +people but powerful for those who want to tinker more with their shell, +the thing used to interface with most of the system. + +The motivation for choosing Lua was that its simpler and better to use +than old shell script. It's fine for basic interactive shell uses, +but that's the only place Hilbish has shell script; everything else is Lua +and aims to be infinitely configurable. If something isn't, open an issue! + +%prep +{{{ git_dir_setup_macro }}} +sed -i '\|/etc/shells|d' Taskfile.yaml + +%build +go-task + +%install +go-task install PREFIX=%{buildroot}/usr BINDIR=%{buildroot}/%{_bindir} + +%post +if [ "$1" = 1 ]; then + if [ ! -f %{_sysconfdir}/shells ] ; then + echo "%{_bindir}/hilbish" > %{_sysconfdir}/shells + echo "/bin/hilbish" >> %{_sysconfdir}/shells + else + grep -q "^%{_bindir}/hilbish$" %{_sysconfdir}/shells || echo "%{_bindir}/hilbish" >> %{_sysconfdir}/shells + grep -q "^/bin/hilbish$" %{_sysconfdir}/shells || echo "/bin/hilbish" >> %{_sysconfdir}/shells + fi +fi + +%postun +if [ "$1" = 0 ] && [ -f %{_sysconfdir}/shells ] ; then + sed -i '\!^%{_bindir}/hilbish$!d' %{_sysconfdir}/shells + sed -i '\!^/bin/hilbish$!d' %{_sysconfdir}/shells +fi + +%files +%doc README.md +%license LICENSE +%{_bindir}/hilbish +%{_datadir}/hilbish diff --git a/history.go b/history.go index a8eb089..51ccf27 100644 --- a/history.go +++ b/history.go @@ -73,13 +73,13 @@ func newFileHistory(path string) *fileHistory { } } - itms := []string{""} lines := strings.Split(string(data), "\n") + itms := make([]string, len(lines) - 1) for i, l := range lines { if i == len(lines) - 1 { continue } - itms = append(itms, l) + itms[i] = l } f, err := os.OpenFile(path, os.O_APPEND | os.O_WRONLY | os.O_CREATE, 0755) if err != nil { diff --git a/init_windows.go b/init_windows.go index 825069d..e76629b 100644 --- a/init_windows.go +++ b/init_windows.go @@ -1,4 +1,4 @@ -// +build windows +//go:build windows package main diff --git a/job.go b/job.go index 709cc1f..f5bd6f2 100644 --- a/job.go +++ b/job.go @@ -18,6 +18,16 @@ import ( var jobs *jobHandler var jobMetaKey = rt.StringValue("hshjob") +// #type +// #interface jobs +// #property cmd The user entered command string for the job. +// #property running Whether the job is running or not. +// #property id The ID of the job in the job table +// #property pid The Process ID +// #property exitCode The last exit code of the job. +// #property stdout The standard output of the job. This just means the normal logs of the process. +// #property stderr The standard error stream of the process. This (usually) includes error messages of the job. +// The Job type describes a Hilbish job. type job struct { cmd string running bool @@ -110,6 +120,10 @@ func (j *job) getProc() *os.Process { return nil } +// #interface jobs +// #member +// start() +// Starts running the job. func luaStartJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -130,6 +144,10 @@ func luaStartJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } +// #interface jobs +// #member +// stop() +// Stops the job from running. func luaStopJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -148,6 +166,11 @@ func luaStopJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } +// #interface jobs +// #member +// foreground() +// Puts a job in the foreground. This will cause it to run like it was +// executed normally and wait for it to complete. func luaForegroundJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -180,6 +203,10 @@ func luaForegroundJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } +// #interface jobs +// #member +// background() +// Puts a job in the background. This acts the same as initially running a job. func luaBackgroundJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -276,6 +303,13 @@ func (j *jobHandler) stopAll() { } } +// #interface jobs +// background job management +/* +Manage interactive jobs in Hilbish via Lua. + +Jobs are the name of background tasks/commands. A job can be started via +interactive usage or with the functions defined below for use in external runners. */ func (j *jobHandler) loader(rtm *rt.Runtime) *rt.Table { jobMethods := rt.NewTable() jFuncs := map[string]util.LuaExport{ @@ -353,6 +387,11 @@ func jobUserData(j *job) *rt.UserData { return rt.NewUserData(j, jobMeta.AsTable()) } +// #interface jobs +// get(id) -> @Job +// Get a job object via its ID. +// --- @param id number +// --- @returns Job func (j *jobHandler) luaGetJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { j.mu.RLock() defer j.mu.RUnlock() @@ -373,6 +412,18 @@ func (j *jobHandler) luaGetJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext(t.Runtime, rt.UserDataValue(job.ud)), nil } +// #interface jobs +// add(cmdstr, args, execPath) +// Creates a new job. This function does not run the job. This function is intended to be +// used by runners, but can also be used to create jobs via Lua. Commanders cannot be ran as jobs. +// #param cmdstr string String that a user would write for the job +// #param args table Arguments for the commands. Has to include the name of the command. +// #param execPath string Binary to use to run the command. Needs to be an absolute path. +/* +#example +hilbish.jobs.add('go build', {'go', 'build'}, '/usr/bin/go') +#example +*/ func (j *jobHandler) luaAddJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.CheckNArgs(3); err != nil { return nil, err @@ -402,6 +453,10 @@ func (j *jobHandler) luaAddJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext1(t.Runtime, rt.UserDataValue(jb.ud)), nil } +// #interface jobs +// all() -> table[@Job] +// Returns a table of all job objects. +// #returns table[Job] func (j *jobHandler) luaAllJobs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { j.mu.RLock() defer j.mu.RUnlock() @@ -414,6 +469,10 @@ func (j *jobHandler) luaAllJobs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext1(t.Runtime, rt.TableValue(jobTbl)), nil } +// #interface jobs +// disown(id) +// Disowns a job. This simply deletes it from the list of jobs without stopping it. +// #param id number func (j *jobHandler) luaDisownJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -431,6 +490,10 @@ func (j *jobHandler) luaDisownJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } +// #interface jobs +// last() -> @Job +// Returns the last added job to the table. +// #returns Job func (j *jobHandler) luaLastJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { j.mu.RLock() defer j.mu.RUnlock() diff --git a/job_unix.go b/job_unix.go index 5029012..0a038b1 100644 --- a/job_unix.go +++ b/job_unix.go @@ -1,4 +1,4 @@ -// +build darwin linux +//go:build unix package main diff --git a/job_windows.go b/job_windows.go index 140a5d1..26818b5 100644 --- a/job_windows.go +++ b/job_windows.go @@ -1,4 +1,4 @@ -// +build windows +//go:build windows package main diff --git a/libs/lunacolors b/libs/lunacolors index 8467b87..34a57c9 160000 --- a/libs/lunacolors +++ b/libs/lunacolors @@ -1 +1 @@ -Subproject commit 8467b87dd8d49c68b4100b2d129d5f071544b8cf +Subproject commit 34a57c964590f89aa065188a588c7b38aff99c28 diff --git a/lua.go b/lua.go index 419970c..94b7910 100644 --- a/lua.go +++ b/lua.go @@ -23,6 +23,7 @@ func luaInit() { MessageHandler: debuglib.Traceback, }) lib.LoadAll(l) + setupSinkType(l) lib.LoadLibs(l, hilbishLoader) // yes this is stupid, i know @@ -32,24 +33,12 @@ func luaInit() { lib.LoadLibs(l, fs.Loader) lib.LoadLibs(l, terminal.Loader) - cmds := commander.New(l) - // When a command from Lua is added, register it for use - cmds.Events.On("commandRegister", func(args ...interface{}) { - cmdName := args[0].(string) - cmd := args[1].(*rt.Closure) - - commands[cmdName] = cmd - }) - cmds.Events.On("commandDeregister", func(args ...interface{}) { - cmdName := args[0].(string) - - delete(commands, cmdName) - }) + cmds = commander.New(l) lib.LoadLibs(l, cmds.Loader) hooks = bait.New(l) hooks.SetRecoverer(func(event string, handler *bait.Listener, err interface{}) { - fmt.Println("Error in", event, "event:", err) + fmt.Println("Error in `error` hook handler:", err) hooks.Off(event, handler) }) @@ -67,7 +56,7 @@ func luaInit() { } // Add more paths that Lua can require from - err := util.DoString(l, "package.path = package.path .. " + requirePaths) + _, err := util.DoString(l, "package.path = package.path .. " + requirePaths) if err != nil { fmt.Fprintln(os.Stderr, "Could not add Hilbish require paths! Libraries will be missing. This shouldn't happen.") } diff --git a/main.go b/main.go index ee0f584..fd511a9 100644 --- a/main.go +++ b/main.go @@ -2,16 +2,20 @@ package main import ( "bufio" + "errors" "fmt" "io" "os" + "os/exec" "os/user" "path/filepath" "runtime" "strings" + "syscall" "hilbish/util" "hilbish/golibs/bait" + "hilbish/golibs/commander" rt "github.com/arnodel/golua/runtime" "github.com/pborman/getopt" @@ -23,7 +27,6 @@ var ( l *rt.Runtime lr *lineReader - commands = map[string]*rt.Closure{} luaCompletions = map[string]*rt.Closure{} confDir string @@ -31,6 +34,7 @@ var ( curuser *user.User hooks *bait.Bait + cmds *commander.Commander defaultConfPath string defaultHistPath string ) @@ -88,7 +92,7 @@ func main() { interactive = true } - if fileInfo, _ := os.Stdin.Stat(); (fileInfo.Mode() & os.ModeCharDevice) == 0 { + if fileInfo, _ := os.Stdin.Stat(); (fileInfo.Mode() & os.ModeCharDevice) == 0 || !term.IsTerminal(int(os.Stdin.Fd())) { interactive = false } @@ -106,18 +110,26 @@ func main() { } if *verflag { - fmt.Printf("Hilbish %s\n", getVersion()) + fmt.Printf("Hilbish %s\nCompiled with %s\n", getVersion(), runtime.Version()) os.Exit(0) } // Set $SHELL if the user wants to if *setshflag { - os.Setenv("SHELL", os.Args[0]) + os.Setenv("SHELL", "hilbish") + + path, err := exec.LookPath("hilbish") + if err == nil { + os.Setenv("SHELL", path) + } + } - go handleSignals() lr = newLineReader("", false) luaInit() + + go handleSignals() + // If user's config doesn't exixt, if _, err := os.Stat(defaultConfPath); os.IsNotExist(err) && *configflag == defaultConfPath { // Read default from current directory @@ -181,11 +193,18 @@ input: break } if err != nil { - if err != readline.CtrlC { + if err == readline.CtrlC { + fmt.Println("^C") + hooks.Emit("hilbish.cancel") + } else { // If we get a completely random error, print fmt.Fprintln(os.Stderr, err) + if errors.Is(err, syscall.ENOTTY) { + // what are we even doing here? + panic("not a tty") + } + <-make(chan struct{}) } - fmt.Println("^C") continue } var priv bool @@ -284,7 +303,7 @@ func removeDupes(slice []string) []string { func contains(s []string, e string) bool { for _, a := range s { - if a == e { + if strings.ToLower(a) == strings.ToLower(e) { return true } } @@ -319,3 +338,7 @@ func getVersion() string { return v.String() } + +func cut(slice []string, idx int) []string { + return append(slice[:idx], slice[idx + 1:]...) +} diff --git a/module.go b/module.go new file mode 100644 index 0000000..bf4e32a --- /dev/null +++ b/module.go @@ -0,0 +1,93 @@ +package main + +import ( + "plugin" + + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" +) + +// #interface module +// native module loading +// #field paths A list of paths to search when loading native modules. This is in the style of Lua search paths and will be used when requiring native modules. Example: `?.so;?/?.so` +/* +The hilbish.module interface provides a function to load +Hilbish plugins/modules. Hilbish modules are Go-written +plugins (see https://pkg.go.dev/plugin) that are used to add functionality +to Hilbish that cannot be written in Lua for any reason. + +Note that you don't ever need to use the load function that is here as +modules can be loaded with a `require` call like Lua C modules, and the +search paths can be changed with the `paths` property here. + +To make a valid native module, the Go plugin has to export a Loader function +with a signature like so: `func(*rt.Runtime) rt.Value`. + +`rt` in this case refers to the Runtime type at +https://pkg.go.dev/github.com/arnodel/golua@master/runtime#Runtime + +Hilbish uses this package as its Lua runtime. You will need to read +it to use it for a native plugin. + +Here is some code for an example plugin: +```go +package main + +import ( + rt "github.com/arnodel/golua/runtime" +) + +func Loader(rtm *rt.Runtime) rt.Value { + return rt.StringValue("hello world!") +} +``` + +This can be compiled with `go build -buildmode=plugin plugin.go`. +If you attempt to require and print the result (`print(require 'plugin')`), it will show "hello world!" +*/ +func moduleLoader(rtm *rt.Runtime) *rt.Table { + exports := map[string]util.LuaExport{ + "load": {moduleLoad, 2, false}, + } + + mod := rt.NewTable() + util.SetExports(rtm, mod, exports) + + return mod +} + +// #interface module +// load(path) +// Loads a module at the designated `path`. +// It will throw if any error occurs. +// #param path string +func moduleLoad(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.CheckNArgs(1); err != nil { + return nil, err + } + + path, err := c.StringArg(0) + if err != nil { + return nil, err + } + + p, err := plugin.Open(path) + if err != nil { + return nil, err + } + + value, err := p.Lookup("Loader") + if err != nil { + return nil, err + } + + loader, ok := value.(func(*rt.Runtime) rt.Value) + if !ok { + return nil, nil + } + + val := loader(t.Runtime) + + return c.PushingNext1(t.Runtime, val), nil +} diff --git a/nature/commands/bg.lua b/nature/commands/bg.lua index f0aa462..fbb3543 100644 --- a/nature/commands/bg.lua +++ b/nature/commands/bg.lua @@ -1,15 +1,15 @@ local commander = require 'commander' -commander.register('bg', function() +commander.register('bg', function(_, sinks) local job = hilbish.jobs.last() if not job then - print 'bg: no last job' + sinks.out:writeln 'bg: no last job' return 1 end - local err = job.background() + local err = job:background() if err then - print('bg: ' .. err) + sinks.out:writeln('bg: ' .. err) return 2 end end) diff --git a/nature/commands/cat.lua b/nature/commands/cat.lua new file mode 100644 index 0000000..a2375e9 --- /dev/null +++ b/nature/commands/cat.lua @@ -0,0 +1,31 @@ +local commander = require 'commander' +local fs = require 'fs' + +commander.register('cat', function(args, sinks) + local exit = 0 + + if #args == 0 then + sinks.out:writeln [[ +usage: cat [file]...]] + end + + local chunkSize = 2^13 -- 8K buffer size + + for _, fName in ipairs(args) do + local f = io.open(fName) + if f == nil then + exit = 1 + sinks.out:writeln(string.format('cat: %s: no such file or directory', fName)) + goto continue + end + + while true do + local block = f:read(chunkSize) + if not block then break end + sinks.out:write(block) + end + ::continue:: + end + io.flush() + return exit +end) diff --git a/nature/commands/cd.lua b/nature/commands/cd.lua index b4d1041..7cfe4a2 100644 --- a/nature/commands/cd.lua +++ b/nature/commands/cd.lua @@ -4,32 +4,25 @@ local fs = require 'fs' local dirs = require 'nature.dirs' dirs.old = hilbish.cwd() -commander.register('cd', function (args) +commander.register('cd', function (args, sinks) if #args > 1 then - print("cd: too many arguments") + sinks.out:writeln("cd: too many arguments") return 1 - elseif #args > 0 then - local path = args[1]:gsub('$%$','\0'):gsub('${([%w_]+)}', os.getenv) - :gsub('$([%w_]+)', os.getenv):gsub('%z','$'):gsub('^%s*(.-)%s*$', '%1') - - if path == '-' then - path = dirs.old - print(path) - end - dirs.setOld(hilbish.cwd()) - dirs.push(path) - - local ok, err = pcall(function() fs.cd(path) end) - if not ok then - print(err) - return 1 - end - bait.throw('cd', path) - - return end - fs.cd(hilbish.home) - bait.throw('cd', hilbish.home) - dirs.push(hilbish.home) + local path = args[1] and args[1] or hilbish.home + if path == '-' then + path = dirs.old + sinks.out:writeln(path) + end + + dirs.setOld(hilbish.cwd()) + dirs.push(path) + + local ok, err = pcall(function() fs.cd(path) end) + if not ok then + sinks.out:writeln(err) + return 1 + end + bait.throw('cd', path) end) diff --git a/nature/commands/cdr.lua b/nature/commands/cdr.lua index 0438e6f..e6aba36 100644 --- a/nature/commands/cdr.lua +++ b/nature/commands/cdr.lua @@ -3,35 +3,38 @@ local fs = require 'fs' local lunacolors = require 'lunacolors' local dirs = require 'nature.dirs' -commander.register('cdr', function(args) +commander.register('cdr', function(args, sinks) if not args[1] then - print(lunacolors.format [[ + sinks.out:writeln(lunacolors.format [[ cdr: change directory to one which has been recently visied usage: cdr -to get a list of recent directories, use {green}{underline}cdr list{reset}]]) +to get a list of recent directories, use {green}cdr list{reset}]]) return end if args[1] == 'list' then local recentDirs = dirs.recentDirs if #recentDirs == 0 then - print 'No directories have been visited.' + sinks.out:writeln 'No directories have been visited.' return 1 end - print(table.concat(recentDirs, '\n')) + for idx, d in ipairs(dirs.recentDirs) do + if d:find(hilbish.home, 1, true) then d = fs.join('~', d:sub(hilbish.home:len() + 1)) end + sinks.out:writeln(lunacolors.format(string.format('{cyan}%d{reset} %s', idx, d))) + end return end local index = tonumber(args[1]) if not index then - print(string.format('Received %s as index, which isn\'t a number.', index)) + sinks.out:writeln(string.format('Received %s as index, which isn\'t a number.', index)) return 1 end if not dirs.recent(index) then - print(string.format('No recent directory found at index %s.', index)) + sinks.out:writeln(string.format('No recent directory found at index %s.', index)) return 1 end diff --git a/nature/commands/clear.lua b/nature/commands/clear.lua new file mode 100644 index 0000000..68aa197 --- /dev/null +++ b/nature/commands/clear.lua @@ -0,0 +1,7 @@ +local ansikit = require 'ansikit' +local commander = require 'commander' + +commander.register('clear', function() + ansikit.clear(true) + ansikit.cursorTo(0, 0) +end) diff --git a/nature/commands/disown.lua b/nature/commands/disown.lua index f8f144f..6645a0f 100644 --- a/nature/commands/disown.lua +++ b/nature/commands/disown.lua @@ -1,8 +1,8 @@ local commander = require 'commander' -commander.register('disown', function(args) +commander.register('disown', function(args, sinks) if #hilbish.jobs.all() == 0 then - print 'disown: no current job' + sinks.out:writeln 'disown: no current job' return 1 end @@ -10,7 +10,7 @@ commander.register('disown', function(args) if #args < 0 then id = tonumber(args[1]) if not id then - print 'disown: invalid id for job' + sinks.out:writeln 'disown: invalid id for job' return 1 end else @@ -19,7 +19,7 @@ commander.register('disown', function(args) local ok = pcall(hilbish.jobs.disown, id) if not ok then - print 'disown: job does not exist' + sinks.out:writeln 'disown: job does not exist' return 2 end end) diff --git a/nature/commands/doc.lua b/nature/commands/doc.lua index a290cd8..7263fe4 100644 --- a/nature/commands/doc.lua +++ b/nature/commands/doc.lua @@ -1,95 +1,192 @@ +local ansikit = require 'ansikit' local commander = require 'commander' local fs = require 'fs' local lunacolors = require 'lunacolors' +local Greenhouse = require 'nature.greenhouse' +local Page = require 'nature.greenhouse.page' +local docfuncs = require 'nature.doc' -commander.register('doc', function(args) +local function strip(text, ...) + for _, pat in ipairs {...} do + text = text:gsub(pat, '\n') + end + + return text +end + +local function transformHTMLandMD(text) + return strip(text, '|||', '|%-%-%-%-|%-%-%-%-|') + :gsub('|(.-)|(.-)|', function(entry1, entry2) + return string.format('%s - %s', entry1, entry2) + end) + :gsub('
', '{separator}') + :gsub('<.->', '') + --:gsub('^\n\n', '\n') + :gsub('\n%s+\n', '\n\n') + --:gsub(' \n', '\n\n') + :gsub('{{< (%w+) `(.-)` >}}', function(shortcode, text) + return docfuncs.renderInfoBlock(shortcode, text) + end) + :gsub('```(%w+)\n(.-)```', function(lang, text) + return docfuncs.renderCodeBlock(text) + end) + :gsub('```\n(.-)\n```', function(text) + return docfuncs.renderCodeBlock(text) + end) + :gsub('`[^\n].-`', lunacolors.cyan) + :gsub('#+ (.-\n)', function(heading) return lunacolors.blue(lunacolors.bold('→ ' .. heading)) end) + :gsub('%*%*(.-)%*%*', lunacolors.bold) +end + +commander.register('doc', function(args, sinks) local moddocPath = hilbish.dataDir .. '/docs/' - local modDocFormat = [[ -%s -%s -# Functions -]] + local stat = pcall(fs.stat, '.git/refs/heads/extended-job-api') + if stat then + -- hilbish git + moddocPath = './docs/' + end + + local modules = table.map(fs.readdir(moddocPath), function(f) + return lunacolors.underline(lunacolors.blue(string.gsub(f, '.md', ''))) + end) + local doc = [[ +Welcome to Hilbish's documentation viewer! Here you can find +documentation for builtin functions and other things related +to Hilbish. + +Usage: doc
[subdoc] +Available sections: ]] .. table.concat(modules, ', ') + local f + local function handleYamlInfo(d) + local vals = {} + local docs = d + + local valsStr = docs:match '^%-%-%-\n.-\n%-%-%-' + if valsStr then + docs = docs:sub(valsStr:len() + 2, #docs) + local pre = docs:sub(1, 1) + if pre == '\n' then + docs = docs:sub(2) + end + + -- parse vals + local lines = string.split(valsStr, '\n') + for _, line in ipairs(lines) do + local key = line:match '(%w+): ' + local val = line:match '^%w+: (.-)$' + + if key then + vals[key] = val + end + end + end + + --docs = docs:sub(1, #docs - 1) + return docs, vals + end if #args > 0 then local mod = args[1] - local f = io.open(moddocPath .. mod .. '.txt', 'rb') + f = io.open(moddocPath .. mod .. '.md', 'rb') local funcdocs = nil + local subdocName = args[2] if not f then - -- assume subdir - -- dataDir/docs//.txt moddocPath = moddocPath .. mod .. '/' - local subdocName = args[2] if not subdocName then - subdocName = 'index' + subdocName = '_index' end - f = io.open(moddocPath .. subdocName .. '.txt', 'rb') + f = io.open(moddocPath .. subdocName .. '.md', 'rb') + local oldmoddocPath = moddocPath if not f then - print('No documentation found for ' .. mod .. '.') - return + moddocPath = moddocPath .. subdocName:match '%w+' .. '/' + f = io.open(moddocPath .. subdocName .. '.md', 'rb') end - funcdocs = f:read '*a' - local moddocs = table.filter(fs.readdir(moddocPath), function(f) return f ~= 'index.txt' end) - local subdocs = table.map(moddocs, function(fname) - return lunacolors.underline(lunacolors.blue(string.gsub(fname, '.txt', ''))) - end) - if subdocName == 'index' then - funcdocs = funcdocs .. '\nSubdocs: ' .. table.concat(subdocs, ', ') + if not f then + moddocPath = oldmoddocPath .. subdocName .. '/' + subdocName = args[3] or '_index' + f = io.open(moddocPath .. subdocName .. '.md', 'rb') + end + if not f then + sinks.out:writeln('No documentation found for ' .. mod .. '.') + return 1 end end - if not funcdocs then - funcdocs = f:read '*a' - end - local desc = '' - local ok = pcall(require, mod) - local backtickOccurence = 0 - local formattedFuncs = lunacolors.format(funcdocs:sub(1, #funcdocs - 1):gsub('`', function() - backtickOccurence = backtickOccurence + 1 - if backtickOccurence % 2 == 0 then - return '{reset}' - else - return '{underline}{green}' - end - end)) - - if ok then - local props = {} - local propstr = '' - local modDesc = '' - local modmt = getmetatable(require(mod)) - if modmt then - modDesc = modmt.__doc - if modmt.__docProp then - -- not all modules have docs for properties - props = table.map(modmt.__docProp, function(v, k) - return lunacolors.underline(lunacolors.blue(k)) .. ' > ' .. v - end) - end - if #props > 0 then - propstr = '\n# Properties\n' .. table.concat(props, '\n') .. '\n' - end - desc = string.format(modDocFormat, modDesc, propstr) - end - end - print(desc .. formattedFuncs) - f:close() - - return end - local modules = table.map(fs.readdir(moddocPath), function(f) - return lunacolors.underline(lunacolors.blue(string.gsub(f, '.txt', ''))) + + local moddocs = table.filter(fs.readdir(moddocPath), function(f) return f ~= '_index.md' and f ~= 'index.md' end) + local subdocs = table.map(moddocs, function(fname) + return lunacolors.underline(lunacolors.blue(string.gsub(fname, '.md', ''))) end) - io.write [[ -Welcome to Hilbish's doc tool! Here you can find documentation for builtin -functions and other things. + local gh = Greenhouse(sinks.out) + function gh:resize() + local size = terminal.size() + self.region = { + width = size.width, + height = size.height - 1 + } + end + gh:resize() -Usage: doc
[subdoc] -A section is a module or a literal section and a subdoc is a subsection for it. + function gh:render() + local workingPage = self.pages[self.curPage] + local offset = self.offset + if self.isSpecial then + offset = self.specialOffset + workingPage = self.specialPage + end + local size = terminal.size() -Available sections: ]] - io.flush() + self.sink:write(ansikit.getCSI(size.height - 1 .. ';1', 'H')) + self.sink:write(ansikit.getCSI(0, 'J')) + if not self.isSpecial then + if args[1] == 'api' then + self.sink:writeln(workingPage.title) + self.sink:write(lunacolors.format(string.format('{grayBg} ↳ {white}{italic}%s {reset}', workingPage.description or 'No description.'))) + else + self.sink:write(lunacolors.reset(string.format('Viewing doc page %s', moddocPath))) + end + end + end + local backtickOccurence = 0 + local function formatDocText(d) + return transformHTMLandMD(d) + --[[ + return lunacolors.format(d:gsub('`(.-)`', function(t) + return docfuncs.renderCodeBlock(t) + end):gsub('\n#+.-\n', function(t) + local signature = t:gsub('<.->(.-)', '{underline}%1'):gsub('\\', '<') + return '{bold}{yellow}' .. signature .. '{reset}' + end)) + ]]-- + end - print(table.concat(modules, ', ')) + + local doc, vals = handleYamlInfo(#args == 0 and doc or formatDocText(f:read '*a')) + if #moddocs ~= 0 and f then + doc = doc .. '\nSubdocs: ' .. table.concat(subdocs, ', ') .. '\n\n' + end + if f then f:close() end + + local page = Page(vals.title, doc) + page.description = vals.description + gh:addPage(page) + + -- add subdoc pages + for _, sdName in ipairs(moddocs) do + local sdFile = fs.join(sdName, '_index.md') + if sdName:match '.md$' then + sdFile = sdName + end + + local f = io.open(moddocPath .. sdFile, 'rb') + local doc, vals = handleYamlInfo(formatDocText(f:read '*a')) + local page = Page(vals.title or sdName, doc) + page.description = vals.description + gh:addPage(page) + end + ansikit.hideCursor() + gh:initUi() end) diff --git a/nature/commands/exec.lua b/nature/commands/exec.lua new file mode 100644 index 0000000..61ef923 --- /dev/null +++ b/nature/commands/exec.lua @@ -0,0 +1,8 @@ +local commander = require 'commander' + +commander.register('exec', function(args) + if #args == 0 then + return + end + hilbish.exec(args[1]) +end) diff --git a/nature/commands/fg.lua b/nature/commands/fg.lua index a3f1451..c5b6738 100644 --- a/nature/commands/fg.lua +++ b/nature/commands/fg.lua @@ -1,15 +1,15 @@ local commander = require 'commander' -commander.register('fg', function() +commander.register('fg', function(_, sinks) local job = hilbish.jobs.last() if not job then - print 'fg: no last job' + sinks.out:writeln 'fg: no last job' return 1 end - local err = job.foreground() -- waits for job; blocks + local err = job:foreground() -- waits for job; blocks if err then - print('fg: ' .. err) + sinks.out:writeln('fg: ' .. err) return 2 end end) diff --git a/nature/commands/greenhouse.lua b/nature/commands/greenhouse.lua new file mode 100644 index 0000000..9c155b0 --- /dev/null +++ b/nature/commands/greenhouse.lua @@ -0,0 +1,124 @@ +local ansikit = require 'ansikit' +local bait = require 'bait' +local commander = require 'commander' +local hilbish = require 'hilbish' +local lunacolors = require 'lunacolors' +local terminal = require 'terminal' +local Greenhouse = require 'nature.greenhouse' +local Page = require 'nature.greenhouse.page' + +commander.register('greenhouse', function(args, sinks) + local gh = Greenhouse(sinks.out) + + local buffer = '' + local display = '' + local command = false + local commands = { + q = function() + gh.keybinds['Ctrl-D'](gh) + end, + ['goto'] = function(args) + if not args[1] then + return 'nuh uh' + end + gh:jump(tonumber(args[1])) + end + } + + function gh:resize() + local size = terminal.size() + self.region = { + width = size.width, + height = size.height - 2 + } + end + + function gh:render() + local workingPage = self.pages[self.curPage] + local offset = self.offset + if self.isSpecial then + offset = self.specialOffset + workingPage = self.specialPage + end + + self.sink:write(ansikit.getCSI(self.region.height + 1 .. ';1', 'H')) + if not self.isSpecial then + self.sink:writeln(lunacolors.format(string.format('{grayBg} ↳ Page %d%s{reset}', self.curPage, workingPage.title and ' — ' .. workingPage.title .. ' ' or ''))) + end + self.sink:write(buffer == '' and display or buffer) + end + function gh:input(c) + -- command handling + if c == ':' and not command then + command = true + end + if c == 'Escape' then + if command then + command = false + buffer = '' + else + if self.isSpecial then gh:special() end + end + elseif c == 'Backspace' then + buffer = buffer:sub(0, -2) + if buffer == '' then + command = false + else + goto update + end + end + + if command then + ansikit.showCursor() + if buffer:match '^:' then buffer = buffer .. c else buffer = c end + else + ansikit.hideCursor() + end + + ::update:: + gh:update() + end + gh:resize() + + gh:keybind('Enter', function(self) + if self.isSpecial then + self:jump(self.specialPageIdx) + self:special(false) + else + if buffer:len() < 2 then return end + + local splitBuf = string.split(buffer, " ") + local command = commands[splitBuf[1]:sub(2)] + if command then + table.remove(splitBuf, 1) + buffer = command(splitBuf) or '' + end + self:update() + end + end) + + if sinks['in'].pipe then + local page = Page('stdin', sinks['in']:readAll()) + gh:addPage(page) + end + + for _, name in ipairs(args) do + local f = io.open(name, 'r') + if not f then + sinks.err:writeln(string.format('could not open file %s', name)) + end + local page = Page(name, f:read '*a') + gh:addPage(page) + end + + if #gh.pages == 0 then + sinks.out:writeln [[greenhouse is the Hilbish pager library and command! +usage: greenhouse ... + +example: greenhouse hello.md]] + return 1 + end + + ansikit.hideCursor() + gh:initUi() +end) diff --git a/nature/completions.lua b/nature/completions.lua index d20cc59..f8127a1 100644 --- a/nature/completions.lua +++ b/nature/completions.lua @@ -24,7 +24,7 @@ function hilbish.completion.handler(line, pos) return {compGroup}, pfx else local ok, compGroups, pfx = pcall(hilbish.completion.call, - 'command.' .. #fields[1], query, ctx, fields) + 'command.' .. fields[1], query, ctx, fields) if ok then return compGroups, pfx end diff --git a/nature/dirs.lua b/nature/dirs.lua index 5b7ec86..328b4b7 100644 --- a/nature/dirs.lua +++ b/nature/dirs.lua @@ -1,3 +1,4 @@ +-- @module dirs local fs = require 'fs' local dirs = {} @@ -11,8 +12,8 @@ dirs.recentDirs = {} dirs.recentSize = 10 --- Get (and remove) a `num` of entries from recent directories. ---- @param num number ---- @param remove boolean Whether to remove items +-- @param num number +-- @param remove boolean Whether to remove items function dirRecents(num, remove) num = num or 1 local entries = {} @@ -34,12 +35,12 @@ function dirRecents(num, remove) end --- Look at `num` amount of recent directories, starting from the latest. ---- @param num? number +-- @param num? number function dirs.peak(num) return dirRecents(num) end ---- Add `d` to the recent directories. +--- Add `d` to the recent directories list. function dirs.push(d) dirs.recentDirs[dirs.recentSize + 1] = nil if dirs.recentDirs[#dirs.recentDirs - 1] ~= d then @@ -50,20 +51,20 @@ function dirs.push(d) end end ---- Remove `num` amount of dirs from the recent directories. ---- @param num number +--- Remove the specified amount of dirs from the recent directories list. +-- @param num number function dirs.pop(num) return dirRecents(num, true) end ---- Get entry from recent directories. ---- @param idx number +--- Get entry from recent directories list based on index. +-- @param idx number function dirs.recent(idx) return dirs.recentDirs[idx] end ---- Sets the old directory. ---- @param d string +--- Sets the old directory string. +-- @param d string function dirs.setOld(d) ok, d = pcall(fs.abs, d) assert(ok, 'could not turn "' .. d .. '"into an absolute path') diff --git a/nature/doc.lua b/nature/doc.lua new file mode 100644 index 0000000..f0b7e11 --- /dev/null +++ b/nature/doc.lua @@ -0,0 +1,47 @@ +local lunacolors = require 'lunacolors' + +local M = {} + +function M.highlight(text) + return text:gsub('\'.-\'', lunacolors.yellow) + --:gsub('%-%- .-', lunacolors.black) +end + +function M.renderCodeBlock(text) + local longest = 0 + local lines = string.split(text:gsub('\t', ' '), '\n') + + for i, line in ipairs(lines) do + local len = line:len() + if len > longest then longest = len end + end + + for i, line in ipairs(lines) do + lines[i] = lunacolors.format('{greyBg}' .. ' ' .. M.highlight(line:sub(0, longest)) + .. string.rep(' ', longest - line:len()) .. ' ') + end + + return '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n' +end + +function M.renderInfoBlock(type, text) + local longest = 0 + local lines = string.split(text:gsub('\t', ' '), '\n') + + for i, line in ipairs(lines) do + local len = line:len() + if len > longest then longest = len end + end + + for i, line in ipairs(lines) do + lines[i] = ' ' .. M.highlight(line:sub(0, longest)) + .. string.rep(' ', longest - line:len()) .. ' ' + end + + local heading + if type == 'warning' then + heading = lunacolors.yellowBg(lunacolors.black(' ⚠ Warning ')) + end + return '\n' .. heading .. '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n' +end +return M diff --git a/nature/greenhouse/init.lua b/nature/greenhouse/init.lua new file mode 100644 index 0000000..fe4c31c --- /dev/null +++ b/nature/greenhouse/init.lua @@ -0,0 +1,365 @@ +-- Greenhouse is a simple text scrolling handler for terminal programs. +-- The idea is that it can be set a region to do its scrolling and paging +-- job and then the user can draw whatever outside it. +-- This reduces code duplication for the message viewer +-- and flowerbook. + +local ansikit = require 'ansikit' +local lunacolors = require 'lunacolors' +local terminal = require 'terminal' +local Page = require 'nature.greenhouse.page' +local Object = require 'nature.object' + +local Greenhouse = Object:extend() + +function Greenhouse:new(sink) + local size = terminal.size() + self.region = size + self.contents = nil -- or can be a table + self.start = 1 -- where to start drawing from (should replace with self.region.y) + self.offset = 1 -- vertical text offset + self.horizOffset = 1 + self.sink = sink + self.pages = {} + self.curPage = 1 + self.step = { + horizontal = 5, + vertical = 1 + } + self.separator = '─' + self.keybinds = { + ['Up'] = function(self) self:scroll 'up' end, + ['Down'] = function(self) self:scroll 'down' end, + ['Left'] = function(self) self:scroll 'left' end, + ['Right'] = function(self) self:scroll 'right' end, + ['Ctrl-Left'] = self.previous, + ['Ctrl-Right'] = self.next, + ['Ctrl-N'] = function(self) self:toc(true) end, + ['Enter'] = function(self) + if self.isSpecial then + self:jump(self.specialPageIdx) + self:special(false) + end + end, + ['Page-Down'] = function(self) self:scroll('down', {page = true}) end, + ['Page-Up'] = function(self) self:scroll('up', {page = true}) end + } + self.isSpecial = false + self.specialPage = nil + self.specialPageIdx = 1 + self.specialOffset = 1 + + return self +end + +function Greenhouse:addPage(page) + table.insert(self.pages, page) +end + +function Greenhouse:updateCurrentPage(text) + local page = self.pages[self.curPage] + page:setText(text) +end + +local ansiPatters = { + '\x1b%[%d+;%d+;%d+;%d+;%d+%w', + '\x1b%[%d+;%d+;%d+;%d+%w', + '\x1b%[%d+;%d+;%d+%w', + '\x1b%[%d+;%d+%w', + '\x1b%[%d+%w' +} + +function Greenhouse:sub(str, offset, limit) + local overhead = 0 + local function addOverhead(s) + overhead = overhead + string.len(s) + end + + local s = str + for _, pat in ipairs(ansiPatters) do + s = s:gsub(pat, addOverhead) + end + + return s:sub(offset, utf8.offset(str, limit + overhead) or limit + overhead) + --return s:sub(offset, limit + overhead) +end + +function Greenhouse:draw() + local workingPage = self.pages[self.curPage] + local offset = self.offset + if self.isSpecial then + offset = self.specialOffset + workingPage = self.specialPage + end + + if workingPage.lazy and not workingPage.loaded then + workingPage.initialize() + end + + local lines = workingPage.lines + self.sink:write(ansikit.getCSI(self.start .. ';1', 'H')) + self.sink:write(ansikit.getCSI(2, 'J')) + + local writer = self.sink.writeln + self.attributes = {} + for i = offset, offset + self.region.height - 1 do + local resetEnd = false + if i > #lines then break end + + if i == offset + self.region.height - 1 then writer = self.sink.write end + + self.sink:write(ansikit.getCSI(self.start + i - offset .. ';1', 'H')) + local line = lines[i]:gsub('{separator}', function() return self.separator:rep(self.region.width - 1) end) + for _, pat in ipairs(ansiPatters) do + line:gsub(pat, function(s) + if s == lunacolors.formatColors.reset then + self.attributes = {} + resetEnd = true + else + --resetEnd = false + --table.insert(self.attributes, s) + end + end) + end + +--[[ + if #self.attributes ~= 0 then + for _, attr in ipairs(self.attributes) do + --writer(self.sink, attr) + end + end +]]-- + + self.sink:write(lunacolors.formatColors.reset) + writer(self.sink, self:sub(line:gsub('\t', ' '), self.horizOffset, self.region.width + self.horizOffset)) + if resetEnd then + self.sink:write(lunacolors.formatColors.reset) + end + end + writer(self.sink, '\27[0m') + self:render() +end + +function Greenhouse:render() +end + +function Greenhouse:scroll(direction, opts) + opts = opts or {} + + if self.isSpecial then + if direction == 'down' then + self:next(true) + elseif direction == 'up' then + self:previous(true) + end + return + end + + local lines = self.pages[self.curPage].lines + + local oldOffset = self.offset + local oldHorizOffset = self.horizOffset + local amount = self.step.vertical + if opts.page then + amount = self.region.height + end + + if direction == 'down' then + self.offset = math.min(self.offset + amount, math.max(1, #lines - self.region.height)) + elseif direction == 'up' then + self.offset = math.max(self.offset - amount, 1) + end + +--[[ + if direction == 'left' then + self.horizOffset = math.max(self.horizOffset - self.step.horizontal, 1) + elseif direction == 'right' then + self.horizOffset = self.horizOffset + self.step.horizontal + end +]]-- + + if self.offset ~= oldOffset then self:draw() end + if self.horizOffset ~= oldHorizOffset then self:draw() end +end + +function Greenhouse:update() + self:resize() + if self.isSpecial then + self:updateSpecial() + end + + self:draw() +end + + +function Greenhouse:special(val) + self.isSpecial = val + self:update() +end + +function Greenhouse:toggleSpecial() + self:special(not self.isSpecial) +end + +--- This function will be called when the special page +--- is on and needs to be updated. +function Greenhouse:updateSpecial() +end + +function Greenhouse:contents() +end + +function Greenhouse:toc(toggle) + if not self.isSpecial then + self.specialPageIdx = self.curPage + end + if toggle then self.isSpecial = not self.isSpecial end + -- Generate a special page for our table of contents + local tocText = string.format([[ +%s + +]], lunacolors.cyan(lunacolors.bold '―― Table of Contents ――')) + + local genericPageCount = 1 + local contents = self:contents() + if contents then + for i, c in ipairs(contents) do + local title = c.title + if c.active then + title = lunacolors.invert(title) + end + + tocText = tocText .. title .. '\n' + end + else + for i, page in ipairs(self.pages) do + local title = page.title + if title == 'Page' then + title = 'Page #' .. genericPageCount + genericPageCount = genericPageCount + 1 + end + if i == self.specialPageIdx then + title = lunacolors.invert(title) + end + + tocText = tocText .. title .. '\n' + end + end + self.specialPage = Page('TOC', tocText) + function self:updateSpecial() + self:toc() + end + self:draw() +end + +function Greenhouse:resize() + local size = terminal.size() + self.region = size +end + +function Greenhouse:next(special) + local oldCurrent = special and self.specialPageIdx or self.curPage + local pageIdx = math.min(oldCurrent + 1, #self.pages) + + if special then + self.specialPageIdx = pageIdx + else + self.curPage = pageIdx + end + + if pageIdx ~= oldCurrent then + self.offset = 1 + self:update() + end +end + +function Greenhouse:previous(special) + local oldCurrent = special and self.specialPageIdx or self.curPage + local pageIdx = math.max(self.curPage - 1, 1) + + if special then + self.specialPageIdx = pageIdx + else + self.curPage = pageIdx + end + + if pageIdx ~= oldCurrent then + self.offset = 1 + self:update() + end +end + +function Greenhouse:jump(idx) + if idx ~= self.curPage then + self.offset = 1 + end + self.curPage = idx + self:update() +end + +function Greenhouse:keybind(key, callback) + self.keybinds[key] = callback +end + +function Greenhouse:input(char) +end + +local function read() + terminal.saveState() + terminal.setRaw() + local c = hilbish.editor.readChar() + + terminal.restoreState() + return c +end + +function Greenhouse:initUi() + local ansikit = require 'ansikit' + local bait = require 'bait' + local commander = require 'commander' + local hilbish = require 'hilbish' + local terminal = require 'terminal' + local Page = require 'nature.greenhouse.page' + local done = false + + local function sigint() + ansikit.clear() + done = true + end + + local function resize() + self:update() + end + bait.catch('signal.sigint', sigint) + + bait.catch('signal.resize', resize) + + ansikit.screenAlt() + ansikit.clear(true) + self:draw() + + while not done do + local c = read() + self:keybind('Ctrl-Q', function() + done = true + end) + self:keybind('Ctrl-D', function() + done = true + end) + + if self.keybinds[c] then + self.keybinds[c](self) + else + self:input(c) + end + end + + ansikit.showCursor() + ansikit.screenMain() + + self = nil + bait.release('signal.sigint', sigint) + bait.release('signal.resize', resize) +end + +return Greenhouse diff --git a/nature/greenhouse/page.lua b/nature/greenhouse/page.lua new file mode 100644 index 0000000..51d1440 --- /dev/null +++ b/nature/greenhouse/page.lua @@ -0,0 +1,32 @@ +local Object = require 'nature.object' + +local Page = Object:extend() + +function Page:new(title, text) + self:setText(text) + self.title = title or 'Page' + self.lazy = false + self.loaded = true + self.children = {} +end + +function Page:setText(text) + self.lines = string.split(text, '\n') +end + +function Page:setTitle(title) + self.title = title +end + +function Page:dynamic(initializer) + self.initializer = initializer + self.lazy = true + self.loaded = false +end + +function Page:initialize() + self.initializer() + self.loaded = true +end + +return Page diff --git a/nature/hummingbird.lua b/nature/hummingbird.lua new file mode 100644 index 0000000..581e92c --- /dev/null +++ b/nature/hummingbird.lua @@ -0,0 +1,84 @@ +local bait = require 'bait' +local commander = require 'commander' +local lunacolors = require 'lunacolors' + +local M = {} +local counter = 0 +local unread = 0 +M._messages = {} +M.icons = { + INFO = '', + SUCCESS = '', + WARN = '', + ERROR = '' +} + +hilbish.messages = {} + +--- Represents a Hilbish message. +--- @class hilbish.message +--- @field icon string Unicode (preferably standard emoji) icon for the message notification. +--- @field title string Title of the message (like an email subject). +--- @field text string Contents of the message. +--- @field channel string Short identifier of the message. `hilbish` and `hilbish.*` is preserved for internal Hilbish messages. +--- @field summary string A short summary of the message. +--- @field read boolean Whether the full message has been read or not. + +function expect(tbl, field) + if not tbl[field] or tbl[field] == '' then + error(string.format('expected field %s in message')) + end +end + +--- Sends a message. +--- @param message hilbish.message +function hilbish.messages.send(message) + expect(message, 'text') + expect(message, 'title') + counter = counter + 1 + unread = unread + 1 + message.index = counter + message.read = false + + M._messages[message.index] = message + bait.throw('hilbish.notification', message) +end + +function hilbish.messages.read(idx) + local msg = M._messages[idx] + if msg then + M._messages[idx].read = true + unread = unread - 1 + end +end + +function hilbish.messages.readAll(idx) + for _, msg in ipairs(hilbish.messages.all()) do + hilbish.messages.read(msg.index) + end +end + +function hilbish.messages.unreadCount() + return unread +end + +function hilbish.messages.delete(idx) + local msg = M._messages[idx] + if not msg then + error(string.format('invalid message index %d', idx or -1)) + end + + M._messages[idx] = nil +end + +function hilbish.messages.clear() + for _, msg in ipairs(hilbish.messages.all()) do + hilbish.messages.delete(msg.index) + end +end + +function hilbish.messages.all() + return M._messages +end + +return M diff --git a/nature/init.lua b/nature/init.lua index df31d8d..a0579d7 100644 --- a/nature/init.lua +++ b/nature/init.lua @@ -1,15 +1,29 @@ -- Prelude initializes everything else for our shell local _ = require 'succulent' -- Function additions +local bait = require 'bait' local fs = require 'fs' package.path = package.path .. ';' .. hilbish.dataDir .. '/?/init.lua' .. ';' .. hilbish.dataDir .. '/?/?.lua' .. ";" .. hilbish.dataDir .. '/?.lua' +hilbish.module.paths = '?.so;?/?.so;' +.. hilbish.userDir.data .. 'hilbish/libs/?/?.so' +.. ";" .. hilbish.userDir.data .. 'hilbish/libs/?.so' + +table.insert(package.searchers, function(module) + local path = package.searchpath(module, hilbish.module.paths) + if not path then return nil end + + -- it didnt work normally, idk + return function() return hilbish.module.load(path) end, path +end) + require 'nature.commands' require 'nature.completions' require 'nature.opts' require 'nature.vim' require 'nature.runner' +require 'nature.hummingbird' local shlvl = tonumber(os.getenv 'SHLVL') if shlvl ~= nil then @@ -28,7 +42,9 @@ do return got_virt end - virt_G[key] = os.getenv(key) + if type(key) == 'string' then + virt_G[key] = os.getenv(key) + end return virt_G[key] end, @@ -54,7 +70,6 @@ do if ok then for _, module in ipairs(modules) do local entry = package.searchpath(module, startSearchPath) - print(entry) if entry then dofile(entry) end @@ -63,3 +78,15 @@ do package.path = package.path .. ';' .. startSearchPath end + +bait.catch('error', function(event, handler, err) + print(string.format('Encountered an error in %s handler\n%s', event, err:sub(8))) +end) + +bait.catch('command.not-found', function(cmd) + print(string.format('hilbish: %s not found', cmd)) +end) + +bait.catch('command.not-executable', function(cmd) + print(string.format('hilbish: %s: not executable', cmd)) +end) diff --git a/nature/object.lua b/nature/object.lua new file mode 100644 index 0000000..053be4a --- /dev/null +++ b/nature/object.lua @@ -0,0 +1,59 @@ +---@class nature.object +---@field super nature.object +local Object = {} +Object.__index = Object + +---Can be overrided by child objects to implement a constructor. +function Object:new() end + +---@return nature.object +function Object:extend() + local cls = {} + for k, v in pairs(self) do + if k:find("__") == 1 then + cls[k] = v + end + end + cls.__index = cls + cls.super = self + setmetatable(cls, self) + return cls +end + +---Check if the object is strictly of the given type. +---@param T any +---@return boolean +function Object:is(T) + return getmetatable(self) == T +end + +---Check if the object inherits from the given type. +---@param T any +---@return boolean +function Object:extends(T) + local mt = getmetatable(self) + while mt do + if mt == T then + return true + end + mt = getmetatable(mt) + end + return false +end + +---Metamethod to get a string representation of an object. +---@return string +function Object:__tostring() + return "Object" +end + +---Methamethod to allow using the object call as a constructor. +---@return nature.object +function Object:__call(...) + local obj = setmetatable({}, self) + obj:new(...) + return obj +end + + +return Object diff --git a/nature/opts/crimmas.lua b/nature/opts/crimmas.lua new file mode 100644 index 0000000..e187113 --- /dev/null +++ b/nature/opts/crimmas.lua @@ -0,0 +1,11 @@ +local lunacolors = require 'lunacolors' + +bait.catch('hilbish.init', function() + + if os.date '%m' == '12' and hilbish.interactive and hilbish.opts.crimmas then + local crimmas = math.random(1, 31) + if crimmas >= 25 and crimmas <= 29 then + print(lunacolors.format '🎄 {green}Merry {red}Christmas{reset} from your {green}favourite{reset} shell {red}(right?){reset} 🌺') + end + end +end) diff --git a/nature/opts/history.lua b/nature/opts/history.lua index f7ab1d7..016b22d 100644 --- a/nature/opts/history.lua +++ b/nature/opts/history.lua @@ -1,5 +1,6 @@ local bait = require 'bait' bait.catch('command.exit', function(_, cmd, priv) + if not cmd then return end if not priv and hilbish.opts.history then hilbish.history.add(cmd) end end) diff --git a/nature/opts/init.lua b/nature/opts/init.lua index ae95ee1..474ea3b 100644 --- a/nature/opts/init.lua +++ b/nature/opts/init.lua @@ -1,22 +1,8 @@ -local opts = {} hilbish.opts = {} -setmetatable(hilbish.opts, { - __newindex = function(_, k, v) - if opts[k] == nil then - error(string.format('opt %s does not exist', k)) - end - - opts[k] = v - end, - __index = function(_, k) - return opts[k] - end -}) - local function setupOpt(name, default) - opts[name] = default - require('nature.opts.' .. name) + hilbish.opts[name] = default + pcall(require, 'nature.opts.' .. name) end local defaultOpts = { @@ -25,7 +11,10 @@ local defaultOpts = { greeting = string.format([[Welcome to {magenta}Hilbish{reset}, {cyan}%s{reset}. The nice lil shell for {blue}Lua{reset} fanatics! ]], hilbish.user), - motd = true + motd = true, + fuzzy = false, + notifyJobFinish = true, + crimmas = true } for optsName, default in pairs(defaultOpts) do diff --git a/nature/opts/motd.lua b/nature/opts/motd.lua index 5f30a6c..c1f31b4 100644 --- a/nature/opts/motd.lua +++ b/nature/opts/motd.lua @@ -2,8 +2,8 @@ local bait = require 'bait' local lunacolors = require 'lunacolors' hilbish.motd = [[ -Hilbish 2.0 is a {red}major{reset} update! If your config doesn't work -anymore, that will definitely be why! A MOTD, very message, much day. +Finally at {red}v2.2!{reset} So much {green}documentation improvements{reset} +and 1 single fix for Windows! {blue}.. and a feature they can't use.{reset} ]] bait.catch('hilbish.init', function() diff --git a/nature/opts/notifyJobFinish.lua b/nature/opts/notifyJobFinish.lua new file mode 100644 index 0000000..a8841a1 --- /dev/null +++ b/nature/opts/notifyJobFinish.lua @@ -0,0 +1,23 @@ +local bait = require 'bait' +local lunacolors = require 'lunacolors' + +bait.catch('job.done', function(job) + if not hilbish.opts.notifyJobFinish then return end + local notifText = string.format(lunacolors.format [[ +Background job with ID#%d has exited (PID %d). +Command string: {bold}{yellow}%s{reset}]], job.id, job.pid, job.cmd) + + if job.stdout ~= '' then + notifText = notifText .. '\n\nStandard output:\n' .. job.stdout + end + if job.stderr ~= '' then + notifText = notifText .. '\n\nStandard error:\n' .. job.stderr + end + + hilbish.messages.send { + channel = 'jobNotify', + title = string.format('Job ID#%d Exited', job.id), + summary = string.format(lunacolors.format 'Background job with command {bold}{yellow}%s{reset} has finished running!', job.cmd), + text = notifText + } +end) diff --git a/nature/runner.lua b/nature/runner.lua index e155f63..235ab77 100644 --- a/nature/runner.lua +++ b/nature/runner.lua @@ -1,3 +1,4 @@ +--- hilbish.runner local currentRunner = 'hybrid' local runners = {} @@ -74,6 +75,12 @@ function hilbish.runner.setCurrent(name) hilbish.runner.setMode(r.run) end +--- Returns the current runner by name. +--- @returns string +function hilbish.runner.getCurrent() + return currentRunner +end + hilbish.runner.add('hybrid', function(input) local cmdStr = hilbish.aliases.resolve(input) diff --git a/os.go b/os.go new file mode 100644 index 0000000..46e3d3c --- /dev/null +++ b/os.go @@ -0,0 +1,26 @@ +package main + +import ( + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" + "github.com/blackfireio/osinfo" +) + +// #interface os +// operating system info +// Provides simple text information properties about the current operating system. +// This mainly includes the name and version. +// #field family Family name of the current OS +// #field name Pretty name of the current OS +// #field version Version of the current OS +func hshosLoader(rtm *rt.Runtime) *rt.Table { + info, _ := osinfo.GetOSInfo() + mod := rt.NewTable() + + util.SetField(rtm, mod, "family", rt.StringValue(info.Family)) + util.SetField(rtm, mod, "name", rt.StringValue(info.Name)) + util.SetField(rtm, mod, "version", rt.StringValue(info.Version)) + + return mod +} diff --git a/pprof.go b/pprof.go new file mode 100644 index 0000000..ac4ed55 --- /dev/null +++ b/pprof.go @@ -0,0 +1,14 @@ +//go:build pprof + +package main + +import ( + _ "net/http/pprof" + "net/http" +) + +func init() { + go func() { + http.ListenAndServe("localhost:8080", nil) + }() +} diff --git a/readline/codes.go b/readline/codes.go index 492bc72..28a9e60 100644 --- a/readline/codes.go +++ b/readline/codes.go @@ -1,5 +1,7 @@ package readline +import "os" + // Character codes const ( charCtrlA = iota + 1 @@ -58,6 +60,8 @@ var ( seqAltF = string([]byte{27, 102}) seqAltR = string([]byte{27, 114}) // Used for alternative history seqAltBackspace = string([]byte{27, 127}) + seqPageUp = string([]byte{27, 91, 53, 126}) + seqPageDown = string([]byte{27, 91, 54, 126}) ) const ( @@ -72,6 +76,8 @@ const ( seqCursorTopLeft = "\x1b[H" // Clears screen and places cursor on top-left seqGetCursorPos = "\x1b6n" // response: "\x1b{Line};{Column}R" + seqHideCursor = "\x1b[?25l" + seqUnhideCursor = "\x1b[?25h" seqCtrlLeftArrow = "\x1b[1;5D" seqCtrlRightArrow = "\x1b[1;5C" @@ -134,3 +140,59 @@ const ( const ( seqCtermFg255 = "\033[48;5;255m" ) + +// TODO: return whether its actually a sequence or not +// remedies the edge case of someone literally typing Ctrl-A for example. +func (rl *Instance) ReadChar() string { + b := make([]byte, 1024) + i, _ := os.Stdin.Read(b) + r := []rune(string(b)) + s := string(r[:i]) + + switch b[0] { + case charCtrlA: return "Ctrl-A" + case charCtrlB: return "Ctrl-B" + case charCtrlC: return "Ctrl-C" + case charEOF: return "Ctrl-D" + case charCtrlE: return "Ctrl-E" + case charCtrlF: return "Ctrl-F" + case charCtrlG: return "Ctrl-G" + case charBackspace, charBackspace2: return "Backspace" + case charTab: return "Tab" + case charCtrlK: return "Ctrl-K" + case charCtrlL: return "Ctrl-L" + case charCtrlN: return "Ctrl-N" + case charCtrlO: return "Ctrl-O" + case charCtrlP: return "Ctrl-P" + case charCtrlQ: return "Ctrl-Q" + case charCtrlR: return "Ctrl-R" + case charCtrlS: return "Ctrl-S" + case charCtrlT: return "Ctrl-T" + case charCtrlU: return "Ctrl-U" + case charCtrlV: return "Ctrl-V" + case charCtrlW: return "Ctrl-W" + case charCtrlX: return "Ctrl-X" + case charCtrlY: return "Ctrl-Y" + case charCtrlZ: return "Ctrl-Z" + case '\r': fallthrough + case '\n': return "Enter" + case charEscape: + switch s { + case string(charEscape): return "Escape" + case seqUp: return "Up" + case seqDown: return "Down" + case seqBackwards: return "Left" + case seqForwards: return "Right" + case seqCtrlLeftArrow: return "Ctrl-Left" + case seqCtrlRightArrow: return "Ctrl-Right" + case seqCtrlDelete, seqCtrlDelete2: return "Ctrl-Delete" + case seqHome, seqHomeSc: return "Home" + case seqEnd, seqEndSc: return "End" + case seqDelete, seqDelete2: return "Delete" + case seqPageUp: return "Page-Up" + case seqPageDown: return "Page-Down" + } + } + + return s +} diff --git a/readline/comp-grid.go b/readline/comp-grid.go index 48a2039..c198bdb 100644 --- a/readline/comp-grid.go +++ b/readline/comp-grid.go @@ -4,7 +4,8 @@ import ( "fmt" "strconv" "strings" -) + "github.com/rivo/uniseg" +) // initGrid - Grid display details. Called each time we want to be sure to have // a working completion group either immediately, or later on. Generally defered. @@ -13,8 +14,8 @@ func (g *CompletionGroup) initGrid(rl *Instance) { // Compute size of each completion item box tcMaxLength := 1 for i := range g.Suggestions { - if len(g.Suggestions[i]) > tcMaxLength { - tcMaxLength = len([]rune(g.Suggestions[i])) + if uniseg.GraphemeClusterCount(g.Suggestions[i]) > tcMaxLength { + tcMaxLength = uniseg.GraphemeClusterCount(g.Suggestions[i]) } } @@ -103,7 +104,7 @@ func (g *CompletionGroup) writeGrid(rl *Instance) (comp string) { rl.tcUsedY++ } - cellWidth := strconv.Itoa((GetTermWidth() / g.tcMaxX) - 2) + cellWidth := strconv.Itoa((GetTermWidth() / g.tcMaxX) - 4) x := 0 y := 1 @@ -124,7 +125,15 @@ func (g *CompletionGroup) writeGrid(rl *Instance) (comp string) { comp += seqInvert } - comp += fmt.Sprintf("%-"+cellWidth+"s %s", fmtEscape(g.Suggestions[i]), seqReset) + sugg := g.Suggestions[i] + if len(sugg) > GetTermWidth() { + sugg = sugg[:GetTermWidth() - 4] + "..." + } + formatStr := "%-"+cellWidth+"s%s " + if g.tcMaxX == 1 { + formatStr = "%s%s" + } + comp += fmt.Sprintf(formatStr, fmtEscape(sugg), seqReset) } // Always add a newline to the group if the end if not punctuated with one diff --git a/readline/comp-group.go b/readline/comp-group.go index 0c53ed1..b2ee4b8 100644 --- a/readline/comp-group.go +++ b/readline/comp-group.go @@ -71,10 +71,9 @@ func (g *CompletionGroup) init(rl *Instance) { // The rx parameter is passed, as the shell already checked that the search pattern is valid. func (g *CompletionGroup) updateTabFind(rl *Instance) { - suggs := make([]string, 0) - + suggs := rl.Searcher(rl.search, g.Suggestions) // We perform filter right here, so we create a new completion group, and populate it with our results. - for i := range g.Suggestions { + /*for i := range g.Suggestions { if rl.regexSearch == nil { continue } if rl.regexSearch.MatchString(g.Suggestions[i]) { suggs = append(suggs, g.Suggestions[i]) @@ -82,7 +81,7 @@ func (g *CompletionGroup) updateTabFind(rl *Instance) { // this is a list so lets also check the descriptions suggs = append(suggs, g.Suggestions[i]) } - } + }*/ // We overwrite the group's items, (will be refreshed as soon as something is typed in the search) g.Suggestions = suggs diff --git a/readline/cursor.go b/readline/cursor.go index f313ef4..9d68a5a 100644 --- a/readline/cursor.go +++ b/readline/cursor.go @@ -1,6 +1,7 @@ package readline import ( +// "fmt" "os" "regexp" "strconv" @@ -68,6 +69,40 @@ func (rl *Instance) getCursorPos() (x int, y int) { // This means that they are not used to keep any reference point when // when we internally move around clearning and printing things +/* +func moveCursorUpBuffered(i int) { + if i < 1 { + return + } + + fmt.Fprintf(rl.bufferedOut, "\x1b[%dA", i) +} + +func moveCursorDownBuffered(i int) { + if i < 1 { + return + } + + fmt.Fprintf(rl.bufferedOut, "\x1b[%dB", i) +} + +func moveCursorForwardsBuffered(i int) { + if i < 1 { + return + } + + fmt.Fprintf(rl.bufferedOut, "\x1b[%dC", i) +} + +func moveCursorUpBuffered(i int) { + if i < 1 { + return + } + + fmt.Fprintf(rl.bufferedOut, "\x1b[%dD", i) +} +*/ + func moveCursorUp(i int) { if i < 1 { return @@ -100,6 +135,14 @@ func moveCursorBackwards(i int) { printf("\x1b[%dD", i) } +func hideCursor() { + print(seqHideCursor) +} + +func unhideCursor() { + print(seqUnhideCursor) +} + func (rl *Instance) backspace(forward bool) { if len(rl.line) == 0 || rl.pos == 0 { return diff --git a/readline/go.mod b/readline/go.mod index ab404cd..d5322dc 100644 --- a/readline/go.mod +++ b/readline/go.mod @@ -1,6 +1,6 @@ module github.com/maxlandon/readline -go 1.16 +go 1.18 require ( github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d diff --git a/readline/history.go b/readline/history.go index 41200c6..0c87a62 100644 --- a/readline/history.go +++ b/readline/history.go @@ -123,23 +123,24 @@ func (rl *Instance) walkHistory(i int) { // When we are exiting the current line buffer to move around // the history, we make buffer the current line - if rl.histPos == 0 && (rl.histPos+i) == 1 { + if rl.histOffset == 0 && rl.histOffset + i == 1 { rl.lineBuf = string(rl.line) } - switch rl.histPos + i { - case 0, history.Len() + 1: - rl.histPos = 0 + rl.histOffset += i + historyLen := history.Len() + if rl.histOffset == 0 { rl.line = []rune(rl.lineBuf) rl.pos = len(rl.lineBuf) - return - case -1: - rl.histPos = 0 - rl.lineBuf = string(rl.line) - default: + } else if rl.histOffset <= -1 { + rl.histOffset = 0 + } else if rl.histOffset > historyLen { + // TODO: should this wrap around?s + rl.histOffset = 0 + } else { dedup = true old = string(rl.line) - new, err = history.GetLine(history.Len() - rl.histPos - 1) + new, err = history.GetLine(historyLen - rl.histOffset) if err != nil { rl.resetHelpers() print("\r\n" + err.Error() + "\r\n") @@ -148,7 +149,6 @@ func (rl *Instance) walkHistory(i int) { } rl.clearLine() - rl.histPos += i rl.line = []rune(new) rl.pos = len(rl.line) if rl.pos > 0 { @@ -160,8 +160,8 @@ func (rl *Instance) walkHistory(i int) { rl.updateHelpers() // In order to avoid having to type j/k twice each time for history navigation, - // we walk once again. This only ever happens when we aren't out of bounds. - if dedup && old == new { + // we walk once again. This only ever happens when we aren't out of bounds and the last history item was not a empty string. + if new != "" && dedup && old == new { rl.walkHistory(i) } } diff --git a/readline/instance.go b/readline/instance.go index fcd8379..163bffe 100644 --- a/readline/instance.go +++ b/readline/instance.go @@ -1,6 +1,7 @@ package readline import ( + "bufio" "os" "regexp" "sync" @@ -112,8 +113,10 @@ type Instance struct { modeAutoFind bool // for when invoked via ^R or ^F outside of [tab] searchMode FindMode // Used for varying hints, and underlying functions called regexSearch *regexp.Regexp // Holds the current search regex match + search string mainHist bool // Which history stdin do we want histInfo []rune // We store a piece of hist info, for dual history sources + Searcher func(string, []string) []string // // History ----------------------------------------------------------------------------------- @@ -134,6 +137,7 @@ type Instance struct { // history operating params lineBuf string histPos int + histOffset int histNavIdx int // Used for quick history navigation. // @@ -200,6 +204,8 @@ type Instance struct { ViActionCallback func(ViAction, []string) RawInputCallback func([]rune) // called on all input + + bufferedOut *bufio.Writer } // NewInstance is used to create a readline instance and initialise it with sane defaults. @@ -228,6 +234,27 @@ func NewInstance() *Instance { rl.HintFormatting = "\x1b[2m" rl.evtKeyPress = make(map[string]func(string, []rune, int) *EventReturn) rl.TempDirectory = os.TempDir() + rl.Searcher = func(needle string, haystack []string) []string { + suggs := make([]string, 0) + + var err error + rl.regexSearch, err = regexp.Compile("(?i)" + string(rl.tfLine)) + if err != nil { + rl.RefreshPromptLog(err.Error()) + rl.infoText = []rune(Red("Failed to match search regexp")) + } + + for _, hay := range haystack { + if rl.regexSearch == nil { continue } + if rl.regexSearch.MatchString(hay) { + suggs = append(suggs, hay) + } + } + + return suggs + } + + rl.bufferedOut = bufio.NewWriter(os.Stdout) // Registers rl.initRegisters() diff --git a/readline/line.go b/readline/line.go index 2024bb0..3069ad8 100644 --- a/readline/line.go +++ b/readline/line.go @@ -33,19 +33,20 @@ func (rl *Instance) GetLine() []rune { func (rl *Instance) echo() { // Then we print the prompt, and the line, + hideCursor() switch { case rl.PasswordMask != 0: case rl.PasswordMask > 0: - print(strings.Repeat(string(rl.PasswordMask), len(rl.line)) + " ") + rl.bufprint(strings.Repeat(string(rl.PasswordMask), len(rl.line)) + " ") default: + // Go back to prompt position, and clear everything below moveCursorBackwards(GetTermWidth()) moveCursorUp(rl.posY) - print(seqClearScreenBelow) // Print the prompt - print(string(rl.realPrompt)) + rl.bufprint(string(rl.realPrompt)) // Assemble the line, taking virtual completions into account var line []rune @@ -57,11 +58,14 @@ func (rl *Instance) echo() { // Print the input line with optional syntax highlighting if rl.SyntaxHighlighter != nil { - print(rl.SyntaxHighlighter(line)) + rl.bufprint(rl.SyntaxHighlighter(line)) } else { - print(string(line)) + rl.bufprint(string(line)) } + rl.bufprint(seqClearScreenBelow) + } + rl.bufflush() // Update references with new coordinates only now, because // the new line may be longer/shorter than the previous one. @@ -72,6 +76,7 @@ func (rl *Instance) echo() { moveCursorUp(rl.fullY) moveCursorDown(rl.posY) moveCursorForwards(rl.posX) + unhideCursor() } func (rl *Instance) insert(r []rune) { @@ -159,7 +164,7 @@ func (rl *Instance) clearLine() { moveCursorForwards(rl.promptLen) // Clear everything after & below the cursor - print(seqClearScreenBelow) + //print(seqClearScreenBelow) // Real input line rl.line = []rune{} diff --git a/readline/prompt.go b/readline/prompt.go index 0f6ca5a..d141cd6 100644 --- a/readline/prompt.go +++ b/readline/prompt.go @@ -48,7 +48,7 @@ func (rl *Instance) RefreshPromptLog(log string) (err error) { rl.stillOnRefresh = true moveCursorUp(rl.infoY + rl.tcUsedY) moveCursorBackwards(GetTermWidth()) - print("\r\n" + seqClearScreenBelow) + //print("\r\n" + seqClearScreenBelow) // Print the log fmt.Printf(log) @@ -97,7 +97,7 @@ func (rl *Instance) RefreshPromptInPlace(prompt string) (err error) { print(seqClearLine) moveCursorUp(rl.infoY + rl.tcUsedY) moveCursorBackwards(GetTermWidth()) - print("\r\n" + seqClearScreenBelow) + //print("\r\n" + seqClearScreenBelow) // Add a new line if needed if rl.Multiline { @@ -137,7 +137,7 @@ func (rl *Instance) RefreshPromptCustom(prompt string, offset int, clearLine boo moveCursorUp(offset) // Then clear everything below our new position - print(seqClearScreenBelow) + //print(seqClearScreenBelow) // Update the prompt if a special has been passed. if prompt != "" { diff --git a/readline/readline.go b/readline/readline.go index 731e297..627bff4 100644 --- a/readline/readline.go +++ b/readline/readline.go @@ -49,7 +49,7 @@ func (rl *Instance) Readline() (string, error) { // History Init // We need this set to the last command, so that we can access it quickly - rl.histPos = 0 + rl.histOffset = 0 rl.viUndoHistory = []undoItem{{line: "", pos: 0}} // Multisplit @@ -238,7 +238,9 @@ func (rl *Instance) Readline() (string, error) { // Normal completion search does only refresh the search pattern and the comps if rl.modeTabFind || rl.modeAutoFind { + rl.resetVirtualComp(false) rl.backspaceTabFind() + rl.renderHelpers() rl.viUndoSkipAppend = true } else { // Always cancel any virtual completion @@ -331,6 +333,8 @@ func (rl *Instance) Readline() (string, error) { rl.modeTabFind = true rl.updateTabFind([]rune{}) + rl.updateVirtualComp() + rl.renderHelpers() rl.viUndoSkipAppend = true // Tab Completion & Completion Search --------------------------------------------------------------- @@ -484,7 +488,10 @@ func (rl *Instance) Readline() (string, error) { if string(r[:i]) != seqShiftTab && string(r[:i]) != seqForwards && string(r[:i]) != seqBackwards && string(r[:i]) != seqUp && string(r[:i]) != seqDown { - rl.resetVirtualComp(false) + // basically only applies except on 1st ctrl r open + // so if we have not explicitly selected something + // (tabCompletionSelect is false) drop virtual completion + rl.resetVirtualComp(!rl.tabCompletionSelect) } } @@ -517,7 +524,9 @@ func (rl *Instance) Readline() (string, error) { if rl.modeAutoFind || rl.modeTabFind { rl.resetVirtualComp(false) rl.updateTabFind(r[:i]) + rl.renderHelpers() rl.viUndoSkipAppend = true + continue } else { rl.resetVirtualComp(false) rl.editorInput(r[:i]) @@ -537,6 +546,10 @@ func (rl *Instance) Readline() (string, error) { // entry readline is currently configured for and then update the line entries // accordingly. func (rl *Instance) editorInput(r []rune) { + if len(r) == 0 { + return + } + switch rl.modeViMode { case VimKeys: rl.vi(r[0]) @@ -604,6 +617,7 @@ func (rl *Instance) escapeSeq(r []rune) { case string(charEscape): switch { case rl.modeAutoFind: + rl.resetVirtualComp(true) rl.resetTabFind() rl.clearHelpers() rl.resetTabCompletion() @@ -611,6 +625,7 @@ func (rl *Instance) escapeSeq(r []rune) { rl.renderHelpers() case rl.modeTabFind: + rl.resetVirtualComp(true) rl.resetTabFind() rl.resetTabCompletion() @@ -853,7 +868,7 @@ func (rl *Instance) escapeSeq(r []rune) { if err != nil { return } - if !rl.mainHist { + if !rl.mainHist && rl.altHistory != nil { line, err = rl.altHistory.GetLine(rl.altHistory.Len() - 1) if err != nil { return diff --git a/readline/tab-virtual.go b/readline/tab-virtual.go index fa7318e..d1e1d76 100644 --- a/readline/tab-virtual.go +++ b/readline/tab-virtual.go @@ -2,6 +2,7 @@ package readline import ( "strings" + "github.com/rivo/uniseg" ) // insertCandidateVirtual - When a completion candidate is selected, we insert it virtually in the input line: @@ -249,10 +250,10 @@ func (rl *Instance) viJumpEVirtual(tokeniser func([]rune, int) ([]string, int, i return case pos >= len(word)-1: word = rTrimWhiteSpace(split[index+1]) - adjust = len(split[index]) - pos - adjust += len(word) - 1 + adjust = uniseg.GraphemeClusterCount(split[index]) - pos + adjust += uniseg.GraphemeClusterCount(word) - 1 default: - adjust = len(word) - pos - 1 + adjust = uniseg.GraphemeClusterCount(word) - pos - 1 } return } diff --git a/readline/tab.go b/readline/tab.go index e6522e6..f2cc140 100644 --- a/readline/tab.go +++ b/readline/tab.go @@ -94,7 +94,7 @@ func (rl *Instance) getTabSearchCompletion() { rl.getCurrentGroup() // Set the info for this completion mode - rl.infoText = append([]rune("Completion search: "), rl.tfLine...) + rl.infoText = append([]rune("Completion search: " + UNDERLINE + BOLD), rl.tfLine...) for _, g := range rl.tcGroups { g.updateTabFind(rl) @@ -102,7 +102,7 @@ func (rl *Instance) getTabSearchCompletion() { // If total number of matches is zero, we directly change the info, and return if comps, _, _ := rl.getCompletionCount(); comps == 0 { - rl.infoText = append(rl.infoText, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...) + rl.infoText = append(rl.infoText, []rune(RESET+DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...) } } @@ -276,13 +276,14 @@ func (rl *Instance) writeTabCompletion() { // than what their MaxLength allows them to, cycling sometimes occur, // but does not fully clears itself: some descriptions are messed up with. // We always clear the screen as a result, between writings. - print(seqClearScreenBelow) + //rl.bufprint(seqClearScreenBelow) // Crop the completions so that it fits within our MaxTabCompleterRows completions, rl.tcUsedY = rl.cropCompletions(completions) // Then we print all of them. - fmt.Printf(completions) + rl.bufprintF(completions) + rl.bufflush() } // cropCompletions - When the user cycles through a completion list longer diff --git a/readline/tabfind.go b/readline/tabfind.go index aa38259..3e46312 100644 --- a/readline/tabfind.go +++ b/readline/tabfind.go @@ -1,9 +1,5 @@ package readline -import ( - "regexp" -) - // FindMode defines how the autocomplete suggestions display type FindMode int @@ -30,15 +26,10 @@ func (rl *Instance) updateTabFind(r []rune) { rl.tfLine = append(rl.tfLine, r...) // The search regex is common to all search modes - var err error - rl.regexSearch, err = regexp.Compile("(?i)" + string(rl.tfLine)) - if err != nil { - rl.RefreshPromptLog(err.Error()) - rl.infoText = []rune(Red("Failed to match search regexp")) - } + rl.search = string(rl.tfLine) // We update and print - rl.clearHelpers() + //rl.clearHelpers() rl.getTabCompletion() rl.renderHelpers() } diff --git a/readline/tui-effects.go b/readline/tui-effects.go index 491ef98..5610b10 100644 --- a/readline/tui-effects.go +++ b/readline/tui-effects.go @@ -14,6 +14,7 @@ var ( // effects BOLD = "\033[1m" DIM = "\033[2m" + UNDERLINE = "\033[4m" RESET = "\033[0m" // colors RED = "\033[31m" diff --git a/readline/update.go b/readline/update.go index 0c2de38..0538aad 100644 --- a/readline/update.go +++ b/readline/update.go @@ -1,12 +1,17 @@ package readline -import "golang.org/x/text/width" +import ( + "fmt" + "strings" + + "golang.org/x/text/width" +) // updateHelpers is a key part of the whole refresh process: // it should coordinate reprinting the input line, any Infos and completions // and manage to get back to the current (computed) cursor coordinates func (rl *Instance) updateHelpers() { - + print(seqHideCursor) // Load all Infos & completions before anything. // Thus overwrites anything having been dirtily added/forced/modified, like rl.SetInfoText() rl.getInfoText() @@ -23,6 +28,7 @@ func (rl *Instance) updateHelpers() { // We are at the prompt line (with the latter // not printed yet), then reprint everything rl.renderHelpers() + print(seqUnhideCursor) } const tabWidth = 4 @@ -52,19 +58,19 @@ func (rl *Instance) updateReferences() { rl.posY = 0 rl.fullY = 0 - var fullLine, cPosLine int + var curLine []rune if len(rl.currentComp) > 0 { - fullLine = getWidth(rl.lineComp) - cPosLine = getWidth(rl.lineComp[:rl.pos]) + curLine = rl.lineComp } else { - fullLine = getWidth(rl.line) - cPosLine = getWidth(rl.line[:rl.pos]) + curLine = rl.line } + fullLine := getWidth(curLine) + cPosLine := getWidth(curLine[:rl.pos]) // We need the X offset of the whole line toEndLine := rl.promptLen + fullLine fullOffset := toEndLine / GetTermWidth() - rl.fullY = fullOffset + rl.fullY = fullOffset + strings.Count(string(curLine), "\n") fullRest := toEndLine % GetTermWidth() rl.fullX = fullRest @@ -190,3 +196,15 @@ func (rl *Instance) renderHelpers() { moveCursorUp(rl.fullY - rl.posY) moveCursorForwards(rl.posX) } + +func (rl *Instance) bufprintF(format string, a ...any) { + fmt.Fprintf(rl.bufferedOut, format, a...) +} + +func (rl *Instance) bufprint(text string) { + fmt.Fprint(rl.bufferedOut, text) +} + +func (rl *Instance) bufflush() { + rl.bufferedOut.Flush() +} diff --git a/readline/vim.go b/readline/vim.go index 886927b..d496705 100644 --- a/readline/vim.go +++ b/readline/vim.go @@ -245,7 +245,7 @@ func (rl *Instance) vi(r rune) { } // Keep the previous cursor position - prev := rl.pos + //prev := rl.pos new, err := rl.StartEditorWithBuffer(multiline, "") if err != nil || len(new) == 0 || string(new) == string(multiline) { @@ -257,11 +257,11 @@ func (rl *Instance) vi(r rune) { // Clean the shell and put the new buffer, with adjusted pos if needed. rl.clearLine() rl.line = new - if prev > len(rl.line) { - rl.pos = len(rl.line) - 1 + rl.pos = len(rl.line) + /*if prev > len(rl.line) { } else { rl.pos = prev - } + }*/ case 'w': // If we were not yanking diff --git a/rl.go b/rl.go index f6cb6cd..231d04b 100644 --- a/rl.go +++ b/rl.go @@ -7,8 +7,9 @@ import ( "hilbish/util" - "github.com/maxlandon/readline" rt "github.com/arnodel/golua/runtime" + "github.com/maxlandon/readline" + "github.com/sahilm/fuzzy" ) type lineReader struct { @@ -24,6 +25,24 @@ func newLineReader(prompt string, noHist bool) *lineReader { rl: rl, } + regexSearcher := rl.Searcher + rl.Searcher = func(needle string, haystack []string) []string { + fz, _ := util.DoString(l, "return hilbish.opts.fuzzy") + fuzz, ok := fz.TryBool() + if !fuzz || !ok { + return regexSearcher(needle, haystack) + } + + matches := fuzzy.Find(needle, haystack) + suggs := make([]string, 0) + + for _, match := range matches { + suggs = append(suggs, match.Str) + } + + return suggs + } + // we don't mind hilbish.read rl instances having completion, // but it cant have shared history if !noHist { @@ -51,11 +70,8 @@ func newLineReader(prompt string, noHist bool) *lineReader { hooks.Emit("hilbish.vimAction", actionStr, args) } rl.HintText = func(line []rune, pos int) []rune { - if hinter == nil { - return []rune{} - } - - retVal, err := rt.Call1(l.MainThread(), rt.FunctionValue(hinter), + hinter := hshMod.Get(rt.StringValue("hinter")) + retVal, err := rt.Call1(l.MainThread(), hinter, rt.StringValue(string(line)), rt.IntValue(int64(pos))) if err != nil { fmt.Println(err) @@ -70,10 +86,8 @@ func newLineReader(prompt string, noHist bool) *lineReader { return []rune(hintText) } rl.SyntaxHighlighter = func(line []rune) string { - if highlighter == nil { - return string(line) - } - retVal, err := rt.Call1(l.MainThread(), rt.FunctionValue(highlighter), + highlighter := hshMod.Get(rt.StringValue("highlighter")) + retVal, err := rt.Call1(l.MainThread(), highlighter, rt.StringValue(string(line))) if err != nil { fmt.Println(err) @@ -225,7 +239,11 @@ func (lr *lineReader) Resize() { return } -// lua module +// #interface history +// command history +// The history interface deals with command history. +// This includes the ability to override functions to change the main +// method of saving history. func (lr *lineReader) Loader(rtm *rt.Runtime) *rt.Table { lrLua := map[string]util.LuaExport{ "add": {lr.luaAddHistory, 1, false}, @@ -241,6 +259,10 @@ func (lr *lineReader) Loader(rtm *rt.Runtime) *rt.Table { return mod } +// #interface history +// add(cmd) +// Adds a command to the history. +// #param cmd string func (lr *lineReader) luaAddHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -254,10 +276,18 @@ func (lr *lineReader) luaAddHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) return c.Next(), nil } +// #interface history +// size() -> number +// Returns the amount of commands in the history. +// #eturns number func (lr *lineReader) luaSize(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext1(t.Runtime, rt.IntValue(int64(lr.fileHist.Len()))), nil } +// #interface history +// get(index) +// Retrieves a command from the history based on the `index`. +// #param index number func (lr *lineReader) luaGetHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -272,6 +302,10 @@ func (lr *lineReader) luaGetHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) return c.PushingNext1(t.Runtime, rt.StringValue(cmd)), nil } +// #interface history +// all() -> table +// Retrieves all history as a table. +// #returns table func (lr *lineReader) luaAllHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { tbl := rt.NewTable() size := lr.fileHist.Len() @@ -284,6 +318,9 @@ func (lr *lineReader) luaAllHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) return c.PushingNext1(t.Runtime, rt.TableValue(tbl)), nil } +// #interface history +// clear() +// Deletes all commands from the history. func (lr *lineReader) luaClearHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { lr.fileHist.clear() return c.Next(), nil diff --git a/rpkg.conf b/rpkg.conf new file mode 100644 index 0000000..957dc05 --- /dev/null +++ b/rpkg.conf @@ -0,0 +1,2 @@ +[rpkg] +user_macros = "${git_props:root}/rpkg.macros" diff --git a/rpkg.macros b/rpkg.macros new file mode 100644 index 0000000..dbcf187 --- /dev/null +++ b/rpkg.macros @@ -0,0 +1,25 @@ +function git_short_hash { + short_hash="$(cached git_short_hash)" + + if [ -z "$short_hash" ]; then + short_hash="$(git rev-parse --short HEAD)" + fi + + output "$short_hash" +} + +function git_tag_version { + tag="$(cached git_tag_version)" + + if [ -z "$tag" ]; then + tag="$(git describe --tags --abbrev=0)" + fi + + # Remove the potential prefix of `v` + if [[ $tag =~ ^v[0-9].* ]]; then + tag="${tag:1}" + fi + + tag="${tag/"-"/"."}" + output "$tag" +} diff --git a/runnermode.go b/runnermode.go index b8995cd..55adfdc 100644 --- a/runnermode.go +++ b/runnermode.go @@ -6,6 +6,49 @@ import ( rt "github.com/arnodel/golua/runtime" ) +// #interface runner +// interactive command runner customization +/* The runner interface contains functions that allow the user to change +how Hilbish interprets interactive input. +Users can add and change the default runner for interactive input to any +language or script of their choosing. A good example is using it to +write command in Fennel. + +Runners are functions that evaluate user input. The default runners in +Hilbish can run shell script and Lua code. + +A runner is passed the input and has to return a table with these values. +All are not required, only the useful ones the runner needs to return. +(So if there isn't an error, just omit `err`.) + +- `exitCode` (number): A numerical code to indicate the exit result. +- `input` (string): The user input. This will be used to add +to the history. +- `err` (string): A string to indicate an interal error for the runner. +It can be set to a few special values for Hilbish to throw the right hooks and have a better looking message: + +`[command]: not-found` will throw a command.not-found hook based on what `[command]` is. + +`[command]: not-executable` will throw a command.not-executable hook. +- `continue` (boolean): Whether to prompt the user for more input. + +Here is a simple example of a fennel runner. It falls back to +shell script if fennel eval has an error. +```lua +local fennel = require 'fennel' + +hilbish.runnerMode(function(input) + local ok = pcall(fennel.eval, input) + if ok then + return { + input = input + } + end + + return hilbish.runner.sh(input) +end) +``` +*/ func runnerModeLoader(rtm *rt.Runtime) *rt.Table { exports := map[string]util.LuaExport{ "sh": {shRunner, 1, false}, @@ -19,6 +62,20 @@ func runnerModeLoader(rtm *rt.Runtime) *rt.Table { return mod } +// #interface runner +// setMode(cb) +// This is the same as the `hilbish.runnerMode` function. +// It takes a callback, which will be used to execute all interactive input. +// In normal cases, neither callbacks should be overrided by the user, +// as the higher level functions listed below this will handle it. +// #param cb function +func _runnerMode() {} + +// #interface runner +// sh(cmd) +// Runs a command in Hilbish's shell script interpreter. +// This is the equivalent of using `source`. +// #param cmd string func shRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -28,13 +85,13 @@ func shRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } - input, exitCode, cont, err := execSh(cmd) + _, exitCode, cont, err := execSh(aliases.Resolve(cmd)) var luaErr rt.Value = rt.NilValue if err != nil { luaErr = rt.StringValue(err.Error()) } runnerRet := rt.NewTable() - runnerRet.Set(rt.StringValue("input"), rt.StringValue(input)) + runnerRet.Set(rt.StringValue("input"), rt.StringValue(cmd)) runnerRet.Set(rt.StringValue("exitCode"), rt.IntValue(int64(exitCode))) runnerRet.Set(rt.StringValue("continue"), rt.BoolValue(cont)) runnerRet.Set(rt.StringValue("err"), luaErr) @@ -42,6 +99,11 @@ func shRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext(t.Runtime, rt.TableValue(runnerRet)), nil } +// #interface runner +// lua(cmd) +// Evaluates `cmd` as Lua input. This is the same as using `dofile` +// or `load`, but is appropriated for the runner interface. +// #param cmd string func luaRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err diff --git a/signal_unix.go b/signal_unix.go index 2e6c885..1564d93 100644 --- a/signal_unix.go +++ b/signal_unix.go @@ -1,4 +1,4 @@ -// +build darwin linux +//go:build unix package main diff --git a/signal_windows.go b/signal_windows.go index 42a9fff..2ed3370 100644 --- a/signal_windows.go +++ b/signal_windows.go @@ -1,4 +1,4 @@ -// +build windows +//go:build windows package main diff --git a/sink.go b/sink.go new file mode 100644 index 0000000..3aa5507 --- /dev/null +++ b/sink.go @@ -0,0 +1,260 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" +) + +var sinkMetaKey = rt.StringValue("hshsink") + +// #type +// A sink is a structure that has input and/or output to/from +// a desination. +type sink struct{ + writer *bufio.Writer + reader *bufio.Reader + file *os.File + ud *rt.UserData + autoFlush bool +} + +func setupSinkType(rtm *rt.Runtime) { + sinkMeta := rt.NewTable() + + sinkMethods := rt.NewTable() + sinkFuncs := map[string]util.LuaExport{ + "flush": {luaSinkFlush, 1, false}, + "read": {luaSinkRead, 1, false}, + "readAll": {luaSinkReadAll, 1, false}, + "autoFlush": {luaSinkAutoFlush, 2, false}, + "write": {luaSinkWrite, 2, false}, + "writeln": {luaSinkWriteln, 2, false}, + } + util.SetExports(l, sinkMethods, sinkFuncs) + + sinkIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + s, _ := sinkArg(c, 0) + + arg := c.Arg(1) + val := sinkMethods.Get(arg) + + if val != rt.NilValue { + return c.PushingNext1(t.Runtime, val), nil + } + + keyStr, _ := arg.TryString() + + switch keyStr { + case "pipe": + val = rt.BoolValue(false) + if s.file != nil { + fileInfo, _ := s.file.Stat(); + val = rt.BoolValue(fileInfo.Mode() & os.ModeCharDevice == 0) + } + } + + return c.PushingNext1(t.Runtime, val), nil + } + + sinkMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(sinkIndex, "__index", 2, false))) + l.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta)) +} + + +// #member +// readAll() -> string +// --- @returns string +// Reads all input from the sink. +func luaSinkReadAll(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + + s, err := sinkArg(c, 0) + if err != nil { + return nil, err + } + + lines := []string{} + for { + line, err := s.reader.ReadString('\n') + if err != nil { + if err == io.EOF { + break + } + + return nil, err + } + + lines = append(lines, line) + } + + return c.PushingNext1(t.Runtime, rt.StringValue(strings.Join(lines, ""))), nil +} + +// #member +// read() -> string +// --- @returns string +// Reads a liine of input from the sink. +func luaSinkRead(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + + s, err := sinkArg(c, 0) + if err != nil { + return nil, err + } + + str, _ := s.reader.ReadString('\n') + + return c.PushingNext1(t.Runtime, rt.StringValue(str)), nil +} + +// #member +// write(str) +// Writes data to a sink. +func luaSinkWrite(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.CheckNArgs(2); err != nil { + return nil, err + } + + s, err := sinkArg(c, 0) + if err != nil { + return nil, err + } + data, err := c.StringArg(1) + if err != nil { + return nil, err + } + + s.writer.Write([]byte(data)) + if s.autoFlush { + s.writer.Flush() + } + + return c.Next(), nil +} + +// #member +// writeln(str) +// Writes data to a sink with a newline at the end. +func luaSinkWriteln(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.CheckNArgs(2); err != nil { + return nil, err + } + + s, err := sinkArg(c, 0) + if err != nil { + return nil, err + } + data, err := c.StringArg(1) + if err != nil { + return nil, err + } + + s.writer.Write([]byte(data + "\n")) + if s.autoFlush { + s.writer.Flush() + } + + return c.Next(), nil +} + +// #member +// flush() +// Flush writes all buffered input to the sink. +func luaSinkFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + + s, err := sinkArg(c, 0) + if err != nil { + return nil, err + } + + s.writer.Flush() + + return c.Next(), nil +} + +// #member +// autoFlush(auto) +// Sets/toggles the option of automatically flushing output. +// A call with no argument will toggle the value. +// --- @param auto boolean|nil +func luaSinkAutoFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + s, err := sinkArg(c, 0) + if err != nil { + return nil, err + } + + v := c.Arg(1) + if v.Type() != rt.BoolType && v.Type() != rt.NilType { + return nil, fmt.Errorf("#1 must be a boolean") + } + + value := !s.autoFlush + if v.Type() == rt.BoolType { + value = v.AsBool() + } + + s.autoFlush = value + + return c.Next(), nil +} + +func newSinkInput(r io.Reader) *sink { + s := &sink{ + reader: bufio.NewReader(r), + } + s.ud = sinkUserData(s) + + if f, ok := r.(*os.File); ok { + s.file = f + } + + return s +} + +func newSinkOutput(w io.Writer) *sink { + s := &sink{ + writer: bufio.NewWriter(w), + autoFlush: true, + } + s.ud = sinkUserData(s) + + return s +} + +func sinkArg(c *rt.GoCont, arg int) (*sink, error) { + s, ok := valueToSink(c.Arg(arg)) + if !ok { + return nil, fmt.Errorf("#%d must be a sink", arg + 1) + } + + return s, nil +} + +func valueToSink(val rt.Value) (*sink, bool) { + u, ok := val.TryUserData() + if !ok { + return nil, false + } + + s, ok := u.Value().(*sink) + return s, ok +} + +func sinkUserData(s *sink) *rt.UserData { + sinkMeta := l.Registry(sinkMetaKey) + return rt.NewUserData(s, sinkMeta.AsTable()) +} diff --git a/testplugin/testplugin.go b/testplugin/testplugin.go new file mode 100644 index 0000000..2d8a41b --- /dev/null +++ b/testplugin/testplugin.go @@ -0,0 +1,9 @@ +package main + +import ( + rt "github.com/arnodel/golua/runtime" +) + +func Loader(rtm *rt.Runtime) rt.Value { + return rt.StringValue("hello world!") +} diff --git a/testplugin/testplugin.so b/testplugin/testplugin.so new file mode 100644 index 0000000..3c83992 Binary files /dev/null and b/testplugin/testplugin.so differ diff --git a/timer.go b/timer.go index 74d13c4..5d536f5 100644 --- a/timer.go +++ b/timer.go @@ -15,13 +15,19 @@ const ( timerTimeout ) +// #type +// #interface timers +// #property type What type of timer it is +// #property running If the timer is running +// #property duration The duration in milliseconds that the timer will run +// The Job type describes a Hilbish timer. type timer struct{ id int typ timerType running bool dur time.Duration fun *rt.Closure - th *timerHandler + th *timersModule ticker *time.Ticker ud *rt.UserData channel chan struct{} @@ -73,6 +79,10 @@ func (t *timer) stop() error { return nil } +// #interface timers +// #member +// start() +// Starts a timer. func timerStart(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -91,6 +101,10 @@ func timerStart(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } +// #interface timers +// #member +// stop() +// Stops a timer. func timerStop(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err diff --git a/timerhandler.go b/timerhandler.go index 64caff8..0a8e34f 100644 --- a/timerhandler.go +++ b/timerhandler.go @@ -10,10 +10,10 @@ import ( rt "github.com/arnodel/golua/runtime" ) -var timers *timerHandler +var timers *timersModule var timerMetaKey = rt.StringValue("hshtimer") -type timerHandler struct { +type timersModule struct { mu *sync.RWMutex wg *sync.WaitGroup timers map[int]*timer @@ -21,8 +21,8 @@ type timerHandler struct { running int } -func newTimerHandler() *timerHandler { - return &timerHandler{ +func newTimersModule() *timersModule { + return &timersModule{ timers: make(map[int]*timer), latestID: 0, mu: &sync.RWMutex{}, @@ -30,11 +30,11 @@ func newTimerHandler() *timerHandler { } } -func (th *timerHandler) wait() { +func (th *timersModule) wait() { th.wg.Wait() } -func (th *timerHandler) create(typ timerType, dur time.Duration, fun *rt.Closure) *timer { +func (th *timersModule) create(typ timerType, dur time.Duration, fun *rt.Closure) *timer { th.mu.Lock() defer th.mu.Unlock() @@ -54,14 +54,20 @@ func (th *timerHandler) create(typ timerType, dur time.Duration, fun *rt.Closure return t } -func (th *timerHandler) get(id int) *timer { +func (th *timersModule) get(id int) *timer { th.mu.RLock() defer th.mu.RUnlock() return th.timers[id] } -func (th *timerHandler) luaCreate(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +// #interface timers +// create(type, time, callback) -> @Timer +// Creates a timer that runs based on the specified `time`. +// #param type number What kind of timer to create, can either be `hilbish.timers.INTERVAL` or `hilbish.timers.TIMEOUT` +// #param time number The amount of time the function should run in milliseconds. +// #param callback function The function to run for the timer. +func (th *timersModule) luaCreate(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.CheckNArgs(3); err != nil { return nil, err } @@ -83,7 +89,12 @@ func (th *timerHandler) luaCreate(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext1(t.Runtime, rt.UserDataValue(tmr.ud)), nil } -func (th *timerHandler) luaGet(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +// #interface timers +// get(id) -> @Timer +// Retrieves a timer via its ID. +// #param id number +// #returns Timer +func (th *timersModule) luaGet(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } @@ -100,7 +111,29 @@ func (th *timerHandler) luaGet(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } -func (th *timerHandler) loader(rtm *rt.Runtime) *rt.Table { +// #interface timers +// #field INTERVAL Constant for an interval timer type +// #field TIMEOUT Constant for a timeout timer type +// timeout and interval API +/* +If you ever want to run a piece of code on a timed interval, or want to wait +a few seconds, you don't have to rely on timing tricks, as Hilbish has a +timer API to set intervals and timeouts. + +These are the simple functions `hilbish.interval` and `hilbish.timeout` (doc +accessible with `doc hilbish`, or `Module hilbish` on the Website). + +An example of usage: +```lua +local t = hilbish.timers.create(hilbish.timers.TIMEOUT, 5000, function() + print 'hello!' +end) + +t:start() +print(t.running) // true +``` +*/ +func (th *timersModule) loader(rtm *rt.Runtime) *rt.Table { timerMethods := rt.NewTable() timerFuncs := map[string]util.LuaExport{ "start": {timerStart, 1, false}, @@ -141,6 +174,9 @@ func (th *timerHandler) loader(rtm *rt.Runtime) *rt.Table { luaTh := rt.NewTable() util.SetExports(rtm, luaTh, thExports) + util.SetField(rtm, luaTh, "INTERVAL", rt.IntValue(0)) + util.SetField(rtm, luaTh, "TIMEOUT", rt.IntValue(1)) + return luaTh } diff --git a/userdir.go b/userdir.go new file mode 100644 index 0000000..a6c4852 --- /dev/null +++ b/userdir.go @@ -0,0 +1,23 @@ +package main + +import ( + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" +) + +// #interface userDir +// user-related directories +// This interface just contains properties to know about certain user directories. +// It is equivalent to XDG on Linux and gets the user's preferred directories +// for configs and data. +// #field config The user's config directory +// #field data The user's directory for program data +func userDirLoader(rtm *rt.Runtime) *rt.Table { + mod := rt.NewTable() + + util.SetField(rtm, mod, "config", rt.StringValue(confDir)) + util.SetField(rtm, mod, "data", rt.StringValue(userDataDir)) + + return mod +} diff --git a/util/util.go b/util/util.go index d27cfe1..0fcd4b0 100644 --- a/util/util.go +++ b/util/util.go @@ -10,64 +10,30 @@ import ( rt "github.com/arnodel/golua/runtime" ) -// Document adds a documentation string to a module. -// It is accessible via the __doc metatable. -func Document(module *rt.Table, doc string) { - mt := module.Metatable() - - if mt == nil { - mt = rt.NewTable() - module.SetMetatable(mt) - } - - mt.Set(rt.StringValue("__doc"), rt.StringValue(doc)) -} - // SetField sets a field in a table, adding docs for it. // It is accessible via the __docProp metatable. It is a table of the names of the fields. -func SetField(rtm *rt.Runtime, module *rt.Table, field string, value rt.Value, doc string) { +func SetField(rtm *rt.Runtime, module *rt.Table, field string, value rt.Value) { // TODO: ^ rtm isnt needed, i should remove it - SetFieldDoc(module, field, doc) module.Set(rt.StringValue(field), value) } -// SetFieldDoc sets the __docProp metatable for a field on the -// module. -func SetFieldDoc(module *rt.Table, field, doc string) { - mt := module.Metatable() - - if mt == nil { - mt = rt.NewTable() - module.SetMetatable(mt) - } - - docProp := mt.Get(rt.StringValue("__docProp")) - if docProp == rt.NilValue { - docPropTbl := rt.NewTable() - mt.Set(rt.StringValue("__docProp"), rt.TableValue(docPropTbl)) - docProp = mt.Get(rt.StringValue("__docProp")) - } - - docProp.AsTable().Set(rt.StringValue(field), rt.StringValue(doc)) -} - // SetFieldProtected sets a field in a protected table. A protected table // is one which has a metatable proxy to ensure no overrides happen to it. // It sets the field in the table and sets the __docProp metatable on the // user facing table. -func SetFieldProtected(module, realModule *rt.Table, field string, value rt.Value, doc string) { - SetFieldDoc(module, field, doc) +func SetFieldProtected(module, realModule *rt.Table, field string, value rt.Value) { realModule.Set(rt.StringValue(field), value) } // DoString runs the code string in the Lua runtime. -func DoString(rtm *rt.Runtime, code string) error { +func DoString(rtm *rt.Runtime, code string) (rt.Value, error) { chunk, err := rtm.CompileAndLoadLuaChunk("", []byte(code), rt.TableValue(rtm.GlobalEnv())) + var ret rt.Value if chunk != nil { - _, err = rt.Call1(rtm.MainThread(), rt.FunctionValue(chunk)) + ret, err = rt.Call1(rtm.MainThread(), rt.FunctionValue(chunk)) } - return err + return ret, err } // DoFile runs the contents of the file in the Lua runtime. diff --git a/vars.go b/vars.go index 810c1ee..1be257c 100644 --- a/vars.go +++ b/vars.go @@ -11,8 +11,9 @@ var ( // Version info var ( - ver = "v2.0.0" - releaseName = "Hibiscus" + ver = "v2.2.3" + releaseName = "Poppy" + gitCommit string gitBranch string ) diff --git a/vars_darwin.go b/vars_darwin.go index 8ec83ba..43215d5 100644 --- a/vars_darwin.go +++ b/vars_darwin.go @@ -1,4 +1,4 @@ -// +build darwin +//go:build darwin package main diff --git a/vars_linux.go b/vars_unix.go similarity index 91% rename from vars_linux.go rename to vars_unix.go index 815ba6a..f90fa55 100644 --- a/vars_linux.go +++ b/vars_unix.go @@ -1,4 +1,4 @@ -// +build linux +//go:build unix && !darwin package main @@ -14,7 +14,7 @@ var ( .. hilbish.userDir.config .. '/hilbish/?/init.lua;' .. hilbish.userDir.config .. '/hilbish/?/?.lua;' .. hilbish.userDir.config .. '/hilbish/?.lua'` - dataDir = "/usr/share/hilbish" + dataDir = "/usr/local/share/hilbish" preloadPath = dataDir + "/nature/init.lua" sampleConfPath = dataDir + "/.hilbishrc.lua" // Path to default/sample config defaultConfDir = "" diff --git a/vars_windows.go b/vars_windows.go index d1bd7b6..f724fc2 100644 --- a/vars_windows.go +++ b/vars_windows.go @@ -1,4 +1,4 @@ -// +build windows +//go:build windows package main diff --git a/website/archetypes/default.md b/website/archetypes/default.md new file mode 100644 index 0000000..00e77bd --- /dev/null +++ b/website/archetypes/default.md @@ -0,0 +1,6 @@ +--- +title: "{{ replace .Name "-" " " | title }}" +date: {{ .Date }} +draft: true +--- + diff --git a/website/config.toml b/website/config.toml new file mode 100644 index 0000000..42f3d30 --- /dev/null +++ b/website/config.toml @@ -0,0 +1,43 @@ +languageCode = 'en-us' +baseURL = 'https://rosettea.github.io/Hilbish/' +title = 'Hilbish' +theme = 'hsh' +enableGitInfo = true + +[menu] +[[menu.nav]] + identifier = 'home' + name = 'Home' + pageref = '/' + weight = 1 +[[menu.nav]] + identifier = 'install' + name = 'Install' + pageref = '/install' + weight = 2 +[[menu.nav]] + identifier = 'docs' + name = 'Docs' + pageref = '/docs' + weight = 3 +[[menu.nav]] + identifier = 'blog' + name = 'Blog' + pageref = '/blog' + weight = 4 + +[markup.goldmark.renderer] +unsafe = true + +[markup.highlight] +lineNos = true +lineNumbersInTable = false +noClasses = false +codeFences = true +guessSyntax = true +tabWidth = 4 + +[author] + [author.sammyette] + name = 'sammyette' + picture = 'https://avatars1.githubusercontent.com/u/38820196?s=460&u=b9f4efb2375bae6cb30656d790c6e0a2939327c0&v=4' diff --git a/website/content/_index.md b/website/content/_index.md new file mode 100644 index 0000000..51b0851 --- /dev/null +++ b/website/content/_index.md @@ -0,0 +1,88 @@ +--- +description: 'Something Unique. Hilbish is the new interactive shell for Lua fans. Extensible, scriptable, configurable: All in Lua.' +--- + +[//]: <> + + +
+

Something Unique.

+

+ 🌺 Hilbish is the new Moon-powered interactive shell for Lua fans!
+ Extensible, scriptable, configurable: All in Lua. ✨ +

+ Install + GitHub +
+ +
+ +
+
+
+
+
Simple and Easy Scripting
+

Hilbish is configured and scripted in the + Lua programming language. This removes all the old, ugly things + about Shell script and introduces everything good about Lua, + including other languages (Fennel, Lua derivatives).

+
+
+
+
+
+
+
History and Completion Menus
+

Hilbish provides the user with proper menus + for completions and history usage. Want to see your previous commands? + Hit Ctrl-R.

+
+
+
+
+
+
+
Tons of Features, and More to Come
+

Hilbish offers a bunch of features to make your + interactive shell experience rich. Things like syntax highlighting + and hinting available via the Lua API.

+
+
+
+
+
+
+
Customizable and Extensible via an accessible Lua API
+

Want to change the language used for interactive input? + Or maybe monitor and notify for the output of background jobs? + Have a real-time updating prompt? All this can be done in Hilbish!

+
+
+
+
+ +
+ +

Why not just Lua?

+

+ Hilbish is your interactive shell as well as a just a Lua interpreter + and enhanced REPL.
+

+
    +
  • Batteries included Lua runtime that's also your user shell!
  • +
  • Provides cross-platform and OS agnostic APIs to ensure your Lua code works everywhere Hilbish does, as expected.
  • +
+ +
+ +

Try It Today!

+

+ Hilbish is known to run on the 3 major platforms (Windows, MacOS, Linux) + but likely builds on other Unixes! Windows doesn't work as well as it should, + so if you're a Windows user, + say something! +

+

diff --git a/website/content/blog/improving-this-website.md b/website/content/blog/improving-this-website.md new file mode 100644 index 0000000..0cff464 --- /dev/null +++ b/website/content/blog/improving-this-website.md @@ -0,0 +1,66 @@ +--- +title: "Improving Hilbish's Branding" +date: 2023-04-13T22:15:31-04:00 +draft: false +--- + +Happy birthday Hilbish! As of last month, Hilbish is now 2 years old. +Unfortunately I missed the official date, but I will still make a more +focused post on the date (19st). + +I decided to fix up this website and Hilbish's logo, so that can +be thought of as something for the 2 years milestone? + +# Logo +Hilbish's old logo was.. not that good. It definitely functioned +as a logo, but the yellow part of it looked ugly (sorry old logo). + + +
+ +You would have definitely seen the new logo, since it is currently +in use on the navigation bar and footer. Here it is in a bigger view: + + +
+ +# Website +Ever since this website was first made, from the release of v2.0, it has +been doing it's job of being a website good enough, but there were a few issues. + +# Padding +Padding is very important! The edges of your screen need space to do nothing, +after all. On mobile or screens small enough, there would not be enough space +for the auto margin to fill, and since there was no padding besides that, +it means things would look a bit cramped. This was simple to fix. + +Here it is before: +![Before](https://safe.kashima.moe/nupzzalt2oa4.png) + +and after: +![After](https://safe.kashima.moe/r0ox4nazfi0q.png) + +# Docs Navigation +On the docs page, the pages are on the left on desktop. Since +phones are too small to have this content on the side, it stays at the top. +This is a bit counter intuitive since it brings in extra scrolling +when navigating to every page for docs and just doesn't look that good. + +A few months ago I made it collapse with the site wide navigation, but it +was not hidden by default. So a few improvements were made: +- Make the doc navigation hidden by default on mobile, just like site wide navigation +- Make doc navigation have the same look as site wide navigation + +Here's a before: +![](https://safe.kashima.moe/krn0a6qwegdj.png) + +and after: +![](https://safe.kashima.moe/sk11ighz47yb.png) + +Looks a lot better now. + +# Other Changes +If you haven't noticed, I have made other changes to the website. +This includes: +- Borders! Something this simple makes the website look a lot better, especially on mobile. +- More padding and margin everywhere. Home, doc pages, blog post listing. diff --git a/website/content/blog/v2.0-release.md b/website/content/blog/v2.0-release.md new file mode 100644 index 0000000..23b8f6f --- /dev/null +++ b/website/content/blog/v2.0-release.md @@ -0,0 +1,114 @@ +--- +title: "Hilbish v2.0 Release" +date: 2022-12-29T01:55:21+00:00 +--- + +Hilbish v2.0 has been released! +Well actually, it was released a week ago, but I only wrote this +Hilbish blog *after* that. + +This is a **big** release, coming 9 months after the previous v1.2.0 and +featuring over 40+ bug fixes and tons of new features and enhancements, so +let's see what is in this release. + +# Documentation +When querying about the problems people have with Hilbish, one of the +issues was its poor documentation. Hilbish had plain text, autogenerated +documentation which only covered the module functions (bait, hilbish, +commander, etc.) and did not include the interfaces (`hilbish.timers`, +`hilbish.jobs` and all that). + +I have tried to improve this by working on documenting all the +interfaces (except for some functions of `hilbish.runner`, that's hard to do) +and made the documentation markdown for use on this website. This means +that users can look at documentation here or with the `doc` command. + +Hopefully this addresses documentation complaints, and if not, please open an issue. + +# Main Bug Fixes +As this is a piece of software with no unit testing that is maintained by me alone, +there is gonna be either some bug or something that I overlooked when +making a change. I make a lot of mistakes. There's also the other fact that +sometimes there's just bugs for any other reasosn. Good thing I fixed +more than 40 of those bugs in this release! + +## Readline Bug Fixes +The pure Go readline library is good in some ways and bad in others. +A good portion of the bug fixes are for the readline library, and also +related to text input with east asian characters and the like (Korean, Japanese, +etc.) + +A few of the fixes (and additions) include: + +- Fixing various crashes, including when there is a "stray" newline at the end of text +- Grid completion menu causing spam and duplicate text when there are items longer than +the terminal and/or contain Japanese or other characters. +- Cursor positioning with CJK characters +- Adding new keybinds and fixing others + +## Other fixes +There are a lot more fixes, even more than the ones listed here, but these are the main ones: + - Don't put alias expanded command in history (I've fixed this 5 times now....) + - Handle stdin being nonblocking + - Completion related fixes, like showing the full name, completing files with spaces + +# Breaking changes +This release is a major version bump not only because there are tons of fixes, but because +there are breaking changes. This means that there are some changes done which would +cause errors with an old user config (breaking). + +## Lua 5.4 +The most important is the use of a new Lua VM library. Previously, Hilbish +used gopher-lua, which implements Lua 5.1. This has been changed to +[golua](https://github.com/arnodel/golua/), which implements Lua 5.4. + +Moving from 5.1 to 5.4 does have breaking changes even if it doesn't seem like it, +and since these are different Lua implementations, there may be some differences there too. + +## Userdata +Previously, objects such as jobs or timers were represented by tables. +This has been changed to userdata to make more sense. + +## Other changes +Runner functions are now required to return a table. +It can (at the moment) have 4 variables: + - `input` (user input) + - `exitCode` (exit code) + - `error` (error message) + - `continue` (whether to prompt for more input) +User input has been added to the return to account for runners wanting to +prompt for continued input, and to add it properly to history. `continue` +got added so that it would be easier for runners to get continued input +without having to actually handle it at all. + +The MacOS config paths now match Linux, since it makes more sense for +a program like Hilbish. + +The Hilbish greeting is now an *opt*, and is printed by default. + +# Feature Additions +Besides fixes and changes, this release also includes a good portion of +new features! Users can now add handlers for syntax highlighting and +inline hinting. + +Some new hooks have been added, like `hilbish.cancel` and `hilbish.init`. +You can look at all the hooks via the `doc hooks` command + +Job management functions have also been added. You can now put jobs in the +foreground/background and disown them via the expected commands and also +via the Lua API. + +The `hilbish.timers` API interface was also added in this release! + +# Closing Off +Hilbish has gone from something small and simple for myself to a slightly +advanced shell with a decent amount of features, and a few users. It +still hasn't reached levels of other alt shells in regards to literally +everything, but the goal is to get there! + +If you want to check the FULL changelog, you can [do so here.](https://github.com/Rosettea/Hilbish/releases/tag/v2.0.0) +This v2.0 release marks an advancement in Hilbish (and also how long +one of my projects hasn't died) and I hope it can advance even further. + +Thanks for reading, and I'll be back for the v2.1 release notes, or maybe +something else in between. diff --git a/website/content/blog/v2.1-release.md b/website/content/blog/v2.1-release.md new file mode 100644 index 0000000..b2e4a17 --- /dev/null +++ b/website/content/blog/v2.1-release.md @@ -0,0 +1,64 @@ +--- +title: "v2.1 Release" +date: 2023-02-07T18:25:38-04:00 +draft: false +--- + +> The release with full changelogs and prebuilt binaries can be +seen at the [v2.1.0](https://github.com/Rosettea/Hilbish/releases/tag/v2.1.0) +tag. + +Oh look! A new release of Hilbish! This time is the v2.1 release, +with a small amount of features and mainly documentation changes and +bug fixes. + +# Documentation +There have been a few documentation enhancements for this release. +This includes: +- Adding the return types for all functions that need them +- Documenting Hilbish types like job objects and timers properly. +They now have a separate heading and listing of properties and methods. +- Fixing outdated documentation + +# Features +## Sinks +A major addition is the new "sink" type for commanders to write +their output to. This was the solution to pipes and other shell +operators not working with builtins. If you wrote a commander +and made it `print`, use `sinks.out:write` instead. + +This is also documented at the [commander docs](./docs/api/commander). + +## `doc` command +Since API documentation has been moved to an API folder and also includes +interfaces, a change has been made to get the module name from the +passed from the requested page. This means that +`doc api hilbish hilbish.jobs` is now shortened to `doc api hilbish.jobs` + +# Bug Fixes +Small release, small amount of bug fixes. Even though, this is the main +part of this release. + +## Completions and Symlinks +Previously Hilbish completions did not work with symlinks properly. +This can be tested in the previous 2.0 release by attempting to +path complete to `/bin`. Since this is (or can be?) a symlink to +`/usr/bin`, it was not marked as a directory and therefore did not +automatically add the ending slash. This has been fixed. + +## Segfaults +I found that when I updated my terminal of choice ([Tym]) for the new +daemon feature, Hilbish would sometimes segfault on startup. This is due +to it getting a resize event on startup while `bait` was not initialized +yet. + +## API Fixes +- The `hilbish.which` function works with aliases. +- `hilbish.completion.files` and `hilbish.completion.bins` will no longer +cause a panic with all empty arguments passed. + +# Next Release +Stay tuned for the v2.2 release, which will have a bigger set of features +and maybe some more bug fixes! + +[Tym]: https://github.com/endaaman/tym diff --git a/website/content/blog/v2.1.1-release.md b/website/content/blog/v2.1.1-release.md new file mode 100644 index 0000000..cea287a --- /dev/null +++ b/website/content/blog/v2.1.1-release.md @@ -0,0 +1,38 @@ +--- +title: "v2.1.1 Release" +date: 2023-04-01T18:15:42-04:00 +draft: false +--- + +> The release with full changelogs and prebuilt binaries can be +seen at the [v2.1.1](https://github.com/Rosettea/Hilbish/releases/tag/v2.1.1) +tag. + +Welcome to a fresh new release of Hilbish! Some people (or none) may be awaiting +the long coming v2.2 release with lots of features, but I *needed* to push +out this little bug fix (wink) release. + +# Bug Fixes +## Validation checks for command input +When running this version, you may have noticed an odd message that sometimes +comes up when running commands. This is from the new TMOLI42SH +(The Meaning of Life is 42 String Hash) input validation scheme. + +## Improved runtime code +Commands now have a chance of taking exactly 2-3s ~~more~~ less time of running due to +improvements in the code for shell runners!!!!! + +## Validate lua code +Hilbish already threw an error when Lua code was not valid in syntax, but there was the +need for an extra validation scheme (called OpTTCLC - Opinion based Turing Test to Check Lua Code) +which results in less time wasted running invalid and TERRIBLE Lua code. + +# Features +There is only 1 new feature in this glorious release. + +## Fix your mistakes for the future +If you run a command that does not exist, Hilbish will say goodbye. + +# Closing +Hope you enjoy this new release! It took a lot of effort to create this new version +while I was busy doing completely nothing. :))) diff --git a/website/content/blog/v2.1.2-release.md b/website/content/blog/v2.1.2-release.md new file mode 100644 index 0000000..4852376 --- /dev/null +++ b/website/content/blog/v2.1.2-release.md @@ -0,0 +1,15 @@ +--- +title: "v2.1.2 Release" +date: 2023-04-10T12:27:41-04:00 +draft: false +--- + +> The release with full changelogs and prebuilt binaries can be +seen at the [v2.1.2](https://github.com/Rosettea/Hilbish/releases/tag/v2.1.2) +tag. + +This release reverts the April Fool's code additions in v2.1.1. It is +functionally equal to v2.1.0. Nice! + +A real release will come possibly in a few days or next week, so stay tuned for +the good and feature-filled release of v2.2! diff --git a/website/content/blog/v2.2-release.md b/website/content/blog/v2.2-release.md new file mode 100644 index 0000000..25f52df --- /dev/null +++ b/website/content/blog/v2.2-release.md @@ -0,0 +1,81 @@ +--- +title: "v2.2 Release" +date: 2023-12-25T23:56:36-04:00 +draft: false +--- + +> The release with full changelogs and prebuilt binaries can be +seen at the [v2.2.0](https://github.com/Rosettea/Hilbish/releases/tag/v2.2.0) +tag. + +Welcome to a very long awaited release of Hilbish, and on Christmas. Just think +of it as a long preparing, late Christmas gift. :) +This release does not contain a whole lot of changes, but it is a new +release with enhancements and bug fixes! + +# Documentation +As is a trend, the documentation has been improved by ONE HUNDRED TIMES. +Okay, not quite, but they've definitely been given an uplift. +Everything has been rewritten, new documentation has been added to both +the website and the local docs accessible with the `doc` command. +Both the website and local docs are now in sync with each other. + +This means that the `doc` command has also been improved to consolidate +the documentation changes. It looks a lot better, has pagination, etc! + +Speaking of pagination... +# Features +## Greenhouse +The first new added feature is the Greenhouse pager! It is a library and +command accessible via `greenhouse`. It will have better integration with +Hilbish things, like notifications and can be used as a base for displaying +multi-line text output instead of paging to less. The usage of Greenhouse is +more efficient and better in Hibish! + +## Notifications +Wait... notifications? Yes! All new in the 2.2 release is a generic notification +interface for things in Hilbish to alert the user of things going on. Stuff like +background jobs finishing, simple alarms, *actual messages*, whatever you like. + +## Fuzzy Searching +Users can now use fuzzy search for command history and completion search. +Enable it with `hilbish.opts.fuzzy = true`! + +### Smaller Enhancements +Did you know of the `cdr` command? I personally don't use it, but I've made +it look slightly better for ease of use. That simple change is adding the indexes +next to the directory so you'll know to type `cdr 2`. + +Users can now add aliases with numbered substitutions. In shell script, +some people have to make functions for certain things that are actually +just aliases. A simple example: `nix run nixpkgs#package` cannot be aliased +because if it was aliased to something like `run` normally there would be a space after. + +Simple fix: +```lua +hilbish.alias('run', 'nix run nixpkgs#%1') +``` + +Rejoice! + +# Bug Fixes +There are a small amount of bug fixes but they're still fixes! + +In some cases Hilbish will panic if: +- Alias resolution results in something empty +- A user does not return a table in a runner functions +These are both fixed. + +An infinite loop has been patched out if someone navigates without +having any prior history. Imagine pressing the up key on a fresh Hilbish +install and you shell no longer working... that's gone now. + +Something else that's gone... is still Windows support, but I added a fix +which will make file completion work now. Job management commands work as +well now too due to an oversight when changing up the job functions. + +# Towards v2.3 +For the next release, I'm hoping that it won't take as long to deliver on +what is realistically a small amount of changes. So v2.3 will be coming +in a short time with some good changes, promise! See you in the +next blog post. diff --git a/website/content/blog/welcome.md b/website/content/blog/welcome.md new file mode 100644 index 0000000..16a878d --- /dev/null +++ b/website/content/blog/welcome.md @@ -0,0 +1,6 @@ +--- +title: "Welcome to the Hilbish blog" +--- + +Hello! Welcome to the Hilbish blog. This will mainly contain release +announcements and some other things relating to Hilbish (development). diff --git a/website/content/docs b/website/content/docs new file mode 120000 index 0000000..92a7f82 --- /dev/null +++ b/website/content/docs @@ -0,0 +1 @@ +../../docs \ No newline at end of file diff --git a/website/content/install.md b/website/content/install.md new file mode 100644 index 0000000..392ded0 --- /dev/null +++ b/website/content/install.md @@ -0,0 +1,60 @@ +--- +title: Install +description: Steps on how to install Hilbish on all the OSes and distros supported. +layout: page +--- + +## Official Binaries +The best way to get Hilbish is to get a build directly from GitHub. +At any time, there are 2 versions of Hilbish recommended for download: +the latest stable release, and development builds from the master branch. + +You can download both at any time, but note that the development builds may +have breaking changes. + +For the latest **stable release**, check here: https://github.com/Rosettea/Hilbish/releases/latest +For a **development build**: https://nightly.link/Rosettea/Hilbish/workflows/build/master + +## Compiling +To read the steps for compiling Hilbish, head over to the [GitHub repository.](https://github.com/Rosettea/Hilbish#build) + +## Package Repositories +Methods of installing Hilbish for your Linux distro. + +### Fedora (COPR) +An official COPR is offered to install Hilbish easily on Fedora. +Enable the repo: +``` +sudo dnf copr enable sammyette/Hilbish +``` + +And install Hilbish: +``` +sudo dnf install hilbish +``` + +Or for the latest development build from master: +``` +sudo dnf install hilbish-git +``` + +### Arch Linux (AUR) +Hilbish is on the AUR. Setup an AUR helper, and install. +Example with yay: + +``` +yay -S hilbish +``` + +Or, from master branch: +``` +yay -S hilbish-git +``` + +### Alpine Linux +Hilbish is currentlty in the testing/edge repository for Alpine. +Follow the steps [here](https://wiki.alpinelinux.org/wiki/Enable_Community_Repository) +(Using testing repositories) and install: +``` +apk add hilbish +``` diff --git a/website/static/completion.mp4 b/website/static/completion.mp4 new file mode 100644 index 0000000..057f9ab Binary files /dev/null and b/website/static/completion.mp4 differ diff --git a/website/static/default.png b/website/static/default.png new file mode 100644 index 0000000..d2cb2c6 Binary files /dev/null and b/website/static/default.png differ diff --git a/website/static/hilbish-flower.png b/website/static/hilbish-flower.png new file mode 100644 index 0000000..866e57e Binary files /dev/null and b/website/static/hilbish-flower.png differ diff --git a/website/static/hilbish-logo-and-text.png b/website/static/hilbish-logo-and-text.png new file mode 100644 index 0000000..325034c Binary files /dev/null and b/website/static/hilbish-logo-and-text.png differ diff --git a/website/static/pillprompt.png b/website/static/pillprompt.png new file mode 100644 index 0000000..c50d675 Binary files /dev/null and b/website/static/pillprompt.png differ diff --git a/website/static/tab.png b/website/static/tab.png new file mode 100644 index 0000000..409d796 Binary files /dev/null and b/website/static/tab.png differ diff --git a/website/static/terminal.png b/website/static/terminal.png new file mode 100644 index 0000000..b2ff38c Binary files /dev/null and b/website/static/terminal.png differ diff --git a/website/themes/hsh/LICENSE b/website/themes/hsh/LICENSE new file mode 100644 index 0000000..da3c8c1 --- /dev/null +++ b/website/themes/hsh/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Rosettea + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/website/themes/hsh/archetypes/default.md b/website/themes/hsh/archetypes/default.md new file mode 100644 index 0000000..ac36e06 --- /dev/null +++ b/website/themes/hsh/archetypes/default.md @@ -0,0 +1,2 @@ ++++ ++++ diff --git a/website/themes/hsh/assets/css/syntax.css b/website/themes/hsh/assets/css/syntax.css new file mode 100644 index 0000000..c4885c0 --- /dev/null +++ b/website/themes/hsh/assets/css/syntax.css @@ -0,0 +1,89 @@ +.chroma { + display: inline-block; + padding: 0.5em; +} +/* Background */ .bg { background-color: #F7F7F7; } +/* PreWrapper */ .chroma { background-color: #F7F7F7; } +/* Other */ .chroma .x { } +/* Error */ .chroma .err { color: #a61717; background-color: #e3d2d2 } +/* CodeLine */ .chroma .cl { } +/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } +/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } +/* LineHighlight */ .chroma .hl { background-color: #F7F7F7 } +/* LineNumbersTable */ .chroma .lnt { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } +/* LineNumbers */ .chroma .ln { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } +/* Line */ .chroma .line { display: flex; } +/* Keyword */ .chroma .k { color: #008800; font-weight: bold } +/* KeywordConstant */ .chroma .kc { color: #008800; font-weight: bold } +/* KeywordDeclaration */ .chroma .kd { color: #008800; font-weight: bold } +/* KeywordNamespace */ .chroma .kn { color: #008800; font-weight: bold } +/* KeywordPseudo */ .chroma .kp { color: #008800 } +/* KeywordReserved */ .chroma .kr { color: #008800; font-weight: bold } +/* KeywordType */ .chroma .kt { color: #888888; font-weight: bold } +/* Name */ .chroma .n { } +/* NameAttribute */ .chroma .na { color: #336699 } +/* NameBuiltin */ .chroma .nb { color: #003388 } +/* NameBuiltinPseudo */ .chroma .bp { } +/* NameClass */ .chroma .nc { color: #bb0066; font-weight: bold } +/* NameConstant */ .chroma .no { color: #003366; font-weight: bold } +/* NameDecorator */ .chroma .nd { color: #555555 } +/* NameEntity */ .chroma .ni { } +/* NameException */ .chroma .ne { color: #bb0066; font-weight: bold } +/* NameFunction */ .chroma .nf { color: #0066bb; font-weight: bold } +/* NameFunctionMagic */ .chroma .fm { } +/* NameLabel */ .chroma .nl { color: #336699; font-style: italic } +/* NameNamespace */ .chroma .nn { color: #bb0066; font-weight: bold } +/* NameOther */ .chroma .nx { } +/* NameProperty */ .chroma .py { color: #336699; font-weight: bold } +/* NameTag */ .chroma .nt { color: #bb0066; font-weight: bold } +/* NameVariable */ .chroma .nv { color: #336699 } +/* NameVariableClass */ .chroma .vc { color: #336699 } +/* NameVariableGlobal */ .chroma .vg { color: #dd7700 } +/* NameVariableInstance */ .chroma .vi { color: #3333bb } +/* NameVariableMagic */ .chroma .vm { } +/* Literal */ .chroma .l { } +/* LiteralDate */ .chroma .ld { } +/* LiteralString */ .chroma .s { color: #dd2200; background-color: #fff0f0 } +/* LiteralStringAffix */ .chroma .sa { color: #dd2200; background-color: #fff0f0 } +/* LiteralStringBacktick */ .chroma .sb { color: #dd2200; background-color: #fff0f0 } +/* LiteralStringChar */ .chroma .sc { color: #dd2200; background-color: #fff0f0 } +/* LiteralStringDelimiter */ .chroma .dl { color: #dd2200; background-color: #fff0f0 } +/* LiteralStringDoc */ .chroma .sd { color: #dd2200; background-color: #fff0f0 } +/* LiteralStringDouble */ .chroma .s2 { color: #dd2200; background-color: #fff0f0 } +/* LiteralStringEscape */ .chroma .se { color: #0044dd; background-color: #fff0f0 } +/* LiteralStringHeredoc */ .chroma .sh { color: #dd2200; background-color: #fff0f0 } +/* LiteralStringInterpol */ .chroma .si { color: #3333bb; background-color: #fff0f0 } +/* LiteralStringOther */ .chroma .sx { color: #22bb22; background-color: #f0fff0 } +/* LiteralStringRegex */ .chroma .sr { color: #008800; background-color: #fff0ff } +/* LiteralStringSingle */ .chroma .s1 { color: #dd2200; background-color: #fff0f0 } +/* LiteralStringSymbol */ .chroma .ss { color: #aa6600; background-color: #fff0f0 } +/* LiteralNumber */ .chroma .m { color: #0000dd; font-weight: bold } +/* LiteralNumberBin */ .chroma .mb { color: #0000dd; font-weight: bold } +/* LiteralNumberFloat */ .chroma .mf { color: #0000dd; font-weight: bold } +/* LiteralNumberHex */ .chroma .mh { color: #0000dd; font-weight: bold } +/* LiteralNumberInteger */ .chroma .mi { color: #0000dd; font-weight: bold } +/* LiteralNumberIntegerLong */ .chroma .il { color: #0000dd; font-weight: bold } +/* LiteralNumberOct */ .chroma .mo { color: #0000dd; font-weight: bold } +/* Operator */ .chroma .o { } +/* OperatorWord */ .chroma .ow { color: #008800 } +/* Punctuation */ .chroma .p { } +/* Comment */ .chroma .c { color: #888888 } +/* CommentHashbang */ .chroma .ch { color: #888888 } +/* CommentMultiline */ .chroma .cm { color: #888888 } +/* CommentSingle */ .chroma .c1 { color: #888888 } +/* CommentSpecial */ .chroma .cs { color: #cc0000; background-color: #fff0f0; font-weight: bold } +/* CommentPreproc */ .chroma .cp { color: #cc0000; font-weight: bold } +/* CommentPreprocFile */ .chroma .cpf { color: #cc0000; font-weight: bold } +/* Generic */ .chroma .g { } +/* GenericDeleted */ .chroma .gd { color: #000000; background-color: #ffdddd } +/* GenericEmph */ .chroma .ge { font-style: italic } +/* GenericError */ .chroma .gr { color: #aa0000 } +/* GenericHeading */ .chroma .gh { color: #333333 } +/* GenericInserted */ .chroma .gi { color: #000000; background-color: #ddffdd } +/* GenericOutput */ .chroma .go { color: #888888 } +/* GenericPrompt */ .chroma .gp { color: #555555 } +/* GenericStrong */ .chroma .gs { font-weight: bold } +/* GenericSubheading */ .chroma .gu { color: #666666 } +/* GenericTraceback */ .chroma .gt { color: #aa0000 } +/* GenericUnderline */ .chroma .gl { text-decoration: underline } +/* TextWhitespace */ .chroma .w { color: #bbbbbb } diff --git a/website/themes/hsh/layouts/404.html b/website/themes/hsh/layouts/404.html new file mode 100644 index 0000000..06b3561 --- /dev/null +++ b/website/themes/hsh/layouts/404.html @@ -0,0 +1,7 @@ +{{ define "main"}} +
+
+

Go Home

+
+
+{{ end }} diff --git a/website/themes/hsh/layouts/_default/_markup/render-heading.html b/website/themes/hsh/layouts/_default/_markup/render-heading.html new file mode 100644 index 0000000..da71fe1 --- /dev/null +++ b/website/themes/hsh/layouts/_default/_markup/render-heading.html @@ -0,0 +1,11 @@ + + + {{ .Text | safeHTML }} + + + + + +{{ if eq .Text ""}} +
+{{ end }} diff --git a/website/themes/hsh/layouts/_default/_markup/render-link.html b/website/themes/hsh/layouts/_default/_markup/render-link.html new file mode 100644 index 0000000..b0d800e --- /dev/null +++ b/website/themes/hsh/layouts/_default/_markup/render-link.html @@ -0,0 +1 @@ +{{ .Text | safeHTML }} diff --git a/website/themes/hsh/layouts/_default/baseof.html b/website/themes/hsh/layouts/_default/baseof.html new file mode 100644 index 0000000..dad9ef8 --- /dev/null +++ b/website/themes/hsh/layouts/_default/baseof.html @@ -0,0 +1,21 @@ + + + {{- partial "head.html" . -}} + + + + + + + + + + + + + + {{- partial "header.html" . -}} + {{- block "main" . }}{{- end }} + {{- partial "footer.html" . -}} + + diff --git a/website/themes/hsh/layouts/_default/doc.html b/website/themes/hsh/layouts/_default/doc.html new file mode 100644 index 0000000..fa7c6b9 --- /dev/null +++ b/website/themes/hsh/layouts/_default/doc.html @@ -0,0 +1,65 @@ +{{ define "main" }} + +
+ + + +
+

{{ .Title }}

+

+ {{ $date := .Date.UTC.Format "Jan 2, 2006" }} + {{ $lastmod := .Lastmod.UTC.Format "Jan 2, 2006" }} + {{ if and (ne $lastmod $date) (gt .Lastmod .Date) }} + Last updated {{ $lastmod }}
+ {{ end }} + + {{ if .Description }} + {{ .Description }}
+ {{ end}} +

+ {{.Content}} +
+ + +
+
+{{ end }} + diff --git a/website/themes/hsh/layouts/_default/list.html b/website/themes/hsh/layouts/_default/list.html new file mode 100644 index 0000000..ac6ea4b --- /dev/null +++ b/website/themes/hsh/layouts/_default/list.html @@ -0,0 +1,21 @@ +{{ define "main" }} +
+
+ {{ range where .Site.RegularPages "Section" "in" "blog" }} +
+
+
+
{{ .Title }}
+
+ {{- if isset .Params "date" -}} + + {{- end -}} +
+

{{if .Description}}{{ .Description }}{{ else }}{{ .Summary }}{{ end }}

+
+
+
+ {{- end }} +
+
+{{ end }} diff --git a/website/themes/hsh/layouts/_default/page.html b/website/themes/hsh/layouts/_default/page.html new file mode 100644 index 0000000..69aaf99 --- /dev/null +++ b/website/themes/hsh/layouts/_default/page.html @@ -0,0 +1,7 @@ +{{ define "main" }} +
+
+ {{.Content}} +
+
+{{ end }} diff --git a/website/themes/hsh/layouts/_default/single.html b/website/themes/hsh/layouts/_default/single.html new file mode 100644 index 0000000..bd7e18c --- /dev/null +++ b/website/themes/hsh/layouts/_default/single.html @@ -0,0 +1,17 @@ +{{ define "main" }} +
+
+

{{ .Title }}

+ + + by {{ .Site.Author.sammyette.name }} + {{- if isset .Params "date" -}} + + {{- end -}} + +
+ {{.Content}} +
+
+
+{{ end }} diff --git a/website/themes/hsh/layouts/index.html b/website/themes/hsh/layouts/index.html new file mode 100644 index 0000000..ef646ab --- /dev/null +++ b/website/themes/hsh/layouts/index.html @@ -0,0 +1,8 @@ +{{ define "main" }} +
+
+ {{.Content}} +
+
+{{ end }} + diff --git a/website/themes/hsh/layouts/partials/footer.html b/website/themes/hsh/layouts/partials/footer.html new file mode 100644 index 0000000..210538a --- /dev/null +++ b/website/themes/hsh/layouts/partials/footer.html @@ -0,0 +1,31 @@ + diff --git a/website/themes/hsh/layouts/partials/head.html b/website/themes/hsh/layouts/partials/head.html new file mode 100644 index 0000000..147fb11 --- /dev/null +++ b/website/themes/hsh/layouts/partials/head.html @@ -0,0 +1,78 @@ + + {{ $title := print .Title " — " .Site.Title }} + {{ if .IsHome }}{{ $title = .Site.Title }}{{ end }} + {{ $title }} + + + + + + + + + + + + + + + + + + + + + + {{ $syntax := resources.Get "css/syntax.css" | resources.Minify | resources.Fingerprint }} + + + + + diff --git a/website/themes/hsh/layouts/partials/header.html b/website/themes/hsh/layouts/partials/header.html new file mode 100644 index 0000000..e8309c4 --- /dev/null +++ b/website/themes/hsh/layouts/partials/header.html @@ -0,0 +1,24 @@ +
+ +
diff --git a/website/themes/hsh/layouts/shortcodes/video.html b/website/themes/hsh/layouts/shortcodes/video.html new file mode 100644 index 0000000..d0f1314 --- /dev/null +++ b/website/themes/hsh/layouts/shortcodes/video.html @@ -0,0 +1,5 @@ + + diff --git a/website/themes/hsh/layouts/shortcodes/warning.html b/website/themes/hsh/layouts/shortcodes/warning.html new file mode 100644 index 0000000..b217b57 --- /dev/null +++ b/website/themes/hsh/layouts/shortcodes/warning.html @@ -0,0 +1,6 @@ + diff --git a/website/themes/hsh/theme.toml b/website/themes/hsh/theme.toml new file mode 100644 index 0000000..a567739 --- /dev/null +++ b/website/themes/hsh/theme.toml @@ -0,0 +1,21 @@ +# theme.toml template for a Hugo theme +# See https://github.com/gohugoio/hugoThemes#themetoml for an example + +name = "Hsh" +license = "MIT" +licenselink = "https://github.com/yourname/yourtheme/blob/master/LICENSE" +description = "" +homepage = "http://example.com/" +tags = [] +features = [] +min_version = "0.41.0" + +[author] + name = "" + homepage = "" + +# If porting an existing theme +[original] + name = "" + homepage = "" + repo = ""