Compare commits

...

100 Commits

Author SHA1 Message Date
TorchedSammy f360671e2a
fix(readline): change input text if it doesnt match supplied prefix for completions 2022-04-20 18:27:29 -04:00
TorchedSammy c6bb8bc663
fix: update for latest changes 2022-04-20 18:18:42 -04:00
TorchedSammy 1458ecdcab
fix: file completions changes
changed the way file completions are handed
completely, which fixes #130 and makes the
full name appear in the completion menu instead
of it being cut off
2022-04-20 13:06:46 -04:00
ym555 6ccb9ebeff
fix(readline): take into account character width (#145)
first step towards unicode support yay
2022-04-20 08:18:34 -04:00
Will Eccles e34ab5314a
fix: make macOS config path match Linux (#143)
Also moves some variables around in vars_*.go to accommodate the fix.
2022-04-19 22:12:05 -04:00
TorchedSammy 0ae31123b9
fix: make expand home actually expand and not do the opposite 2022-04-19 22:05:48 -04:00
TorchedSammy 7a17d7931f
fix: use load from source in DoFile function instead of load chunk 2022-04-19 21:25:52 -04:00
TorchedSammy d4084a82ba
fix: expandHome function using hist dir instead of passed arg 2022-04-19 21:23:48 -04:00
TorchedSammy 9d69b63a0f
chore: update golua (closes #142) 2022-04-19 14:37:57 -04:00
TorchedSammy b83c09a2b3
fix(readline): check stdin error properly 2022-04-19 10:30:04 -04:00
TorchedSammy bee8d6e9e6
fix(readline): input going to next line if its longer than terminal width 2022-04-18 22:42:27 -04:00
TorchedSammy 48cb62282d
fix(readline): input getting cut off on enter 2022-04-18 22:36:54 -04:00
TorchedSammy 1e48a3e03d
fix(readline): home and end buttons not putting the cursor in the right place 2022-04-18 16:04:56 -04:00
TorchedSammy 4e8aa7ed1d
fix(readline): use invert for completion highlight instead of hardcoded colors 2022-04-17 23:39:53 -04:00
TorchedSammy 919a52a630
fix: handle syntax error in aliased command
if an alias is something which isn't valid syntax,
specifically if hilbish cant split up the input
properly to execute, it will report the error to
the user. the previous behaviour was a panic since
on error the args slice will be of length 0

this is basically an edge case and fixes a bug
which shouldnt happen normally
2022-04-17 22:58:29 -04:00
TorchedSammy b0c950a96a
fix: make sure user input is saved to history without alias expansion (4th same regression woo) 2022-04-17 22:57:31 -04:00
TorchedSammy c4438579f6
build: remove all from install target 2022-04-14 10:11:20 -04:00
buffet 59e81d3996
build: improve makefile 2022-04-14 10:05:35 -04:00
Renzix 7f161e6683
feat: added forward/backward word keybinds (#139)
Added emacsForwardWord and emacsBackwardWord which is used by M-f and
M-b directly. Also added M-d to ctrl delete and removed the bad old
funcion in favor of the fancy new one. Lastly I added alt delete which
deletes with emacsBackwardWord. Works identically to gnu readline

Co-authored-by: Renzix <DanielDeBruno@renzix.com>
2022-04-14 07:42:46 -04:00
TorchedSammy ded0be275f
chore: bump version to v2.0.0 2022-04-13 20:50:29 -04:00
TorchedSammy e3fdf84f5c
fix!: make path to script the 0th arg instead of 1st
makes more sense, brings some lua parity
this means that user passed args start from 1
instead of 2
2022-04-13 20:19:28 -04:00
TorchedSammy 194e4e01b7
fix: don't insert any unhandled control keys 2022-04-13 19:36:18 -04:00
TorchedSammy e5c9b85008
feat: add ctrl _ to undo 2022-04-13 16:58:36 -04:00
TorchedSammy e044aeb5ed docs: [ci] generate new docs 2022-04-13 14:14:06 +00:00
sammyette 0a2046e985
feat: add right prompt (#140)
* feat: add right prompt (closes #111)

* chore: add comment for set right prompt function

* fix: add 1 space at the end of right prompt to fix character cut off

* docs: update doc for prompt function
2022-04-13 10:13:46 -04:00
TorchedSammy 626b036b4b
fix!: add complete input to history, including continued input
this introduces a breaking change to runner functions.
they are now required to return 3 values, the first
being the user's input, and the 2 others that it was
before. the `hilbish.runner` functions respectively
have been updated, so if you just return from those
there will be no difference
2022-04-13 10:12:17 -04:00
sammyette ce625aca0c
feat: add ctrl delete to forward delete word (#138)
* feat: add ctrl delete to forward delete word (closes #124)

* fix: make delete word function accurately

* fix: make ctrl delete work on st
2022-04-12 23:08:44 -04:00
TorchedSammy 1715a1f626
feat: make ctrl d delete char below cursor if line isnt empty 2022-04-12 21:02:01 -04:00
TorchedSammy f002eca258
fix: dont prompt for continued input on incomplete input when not interactive (closes #137) 2022-04-12 19:43:12 -04:00
TorchedSammy 2814f44163
fix: typo in timer create function 2022-04-12 19:41:50 -04:00
TorchedSammy ea7517be05 docs: [ci] generate new docs 2022-04-12 23:37:39 +00:00
TorchedSammy 508fd5f8a2
docs: update docs for timer related functions 2022-04-12 19:37:15 -04:00
TorchedSammy c95ff42dee
feat: add timer pool and api (closes #135)
adds a map (but lets call it a pool) of all
running timers. this makes us able to keep
track of all running intervals and timeouts.
it also means hilbish can wait for them to
be done before exiting (it only waits when
non interactive).

this introduces the `hilbish.timers` interface,
documented by `doc timers`. the `hilbish.interval`
and `hilbish.timeout` functions return a timer
object now.
2022-04-12 19:31:48 -04:00
TorchedSammy c342f4f6f5
fix: handle when stdin is in nonblocking mode (closes #136) 2022-04-08 10:46:25 -04:00
TorchedSammy 393fe3962f
chore: update golua 2022-04-05 22:50:14 -04:00
TorchedSammy 8ae22127c0
fix: remove virt g handling at command exit 2022-04-05 07:41:11 -04:00
TorchedSammy 8f942f6f60 docs: [ci] generate new docs 2022-04-05 01:35:10 +00:00
TorchedSammy b712efd278
fix(docgen): make functions that take varargs have the signature 2022-04-04 21:34:46 -04:00
TorchedSammy ee4d97ff9a
fix: put input in history instead of resolved input ran by hilbish
ive fixed this like 3 times and regressed it
2022-04-04 21:21:46 -04:00
TorchedSammy 9ce861b080
refactor: set runner options in a better way and move out exec handler 2022-04-04 21:20:02 -04:00
TorchedSammy 69d38d7048 docs: [ci] generate new docs 2022-04-04 10:40:25 +00:00
sammyette 0fc5f457ad
refactor!: support lua 5.4 (#129)
major rewrite which changes the library hilbish uses for it's lua vm
this one implements lua 5.4, and since that's a major version bump,
it's a breaking change. introduced here also is a fix for `hilbish.login`
not being the right value

* refactor: start work on lua 5.4

lots of commented out code

ive found a go lua library which implements lua 5.4
and found an opportunity to start working on it.
this commit basically removes everything and just leaves
enough for the shell to be "usable" and able to start.
there are no builtins or libraries (besides the `hilbish` global)

* fix: call cont next in prompt function

this continues execution of lua, very obvious
fixes an issue with code stopping at the prompt function

* fix: handle errors in user config

* fix: handle panic in lua input if it is incorrect

* feat: implement bait

* refactor: use util funcs to run lua where possible

* refactor: move arg handle function to util

* feat: implement commander

* feat: implement fs

* feat: add hilbish module functions used by prelude

* chore: use custom fork of golua

* fix: make sure args to setenv are strings in prelude

* feat: implement completions

* chore: remove comment

* feat: implement terminal

* feat: implement hilbish.interval

* chore: update lunacolors

* chore: update golua

* feat: implement aliases

* feat: add input mode

* feat: implement runner mode

* style: use comma separated cases instead of fallthrough

* feat: implement syntax highlight and hints

* chore: add comments to document util functions

* chore: fix dofile comment doc

* refactor: make loader functions for go modules unexported

* feat: implement job management

* feat: add hilbish properties

* feat: implement all hilbish module functions

* feat: implement history interface

* feat: add completion interface

* feat: add module description docs

* feat: implement os interface

* refactor: use hlalias for add function in hilbish.alias interface

* feat: make it so hilbish.run can return command output

* fix: set hilbish.exitCode to last command exit code

* fix(ansikit): flush on io.write

* fix: deregister commander if return isnt number

* feat: run script when provided path

* fix: read file manually in DoFile to avoid shebang

* chore: add comment for reason of unreading byte

* fix: remove prelude error printing

* fix: add names at chunk load for context in errors

* fix: add newline at the beginning of file buffer when there is shebang

this makes the line count in error messages line up properly

* fix: remove extra newline after error
2022-04-04 06:40:02 -04:00
TorchedSammy 64bf7024d2
docs: fix hilbish typo 2022-04-03 21:43:13 -04:00
Renzix 0ebd8d9035
feat: added alt backspace keybinding (#132)
Co-authored-by: Renzix <DanielDeBruno@renzix.com>
2022-03-29 22:15:23 -04:00
Renzix 52caedc1f1
feat: delete key on st and fix: delete key crash on xterm (#131)
* fix: delete key on st

* fix: delete key crash on xterm

Co-authored-by: Renzix <DanielDeBruno@renzix.com>
2022-03-29 20:35:51 -04:00
TorchedSammy 34ae8ade7b
chore: tidy go modules 2022-03-29 15:28:39 -04:00
TorchedSammy 9ff6e5879f
chore: bump go version to 1.17 2022-03-29 13:31:16 -04:00
TorchedSammy 20fae8a870
fix: prompt refresh (closes #116) 2022-03-29 13:07:27 -04:00
TorchedSammy eff942433d
fix!: remove complete global (was supposed to be gone in 1.0) 2022-03-27 21:10:13 -04:00
Renzix 61c9e12a4a
feat: control k to delete the rest of the line (#128)
Co-authored-by: Renzix <DanielDeBruno@renzix.com>
2022-03-26 23:43:30 -04:00
TorchedSammy 0aba60b5de docs: [ci] generate new docs 2022-03-26 22:28:27 +00:00
TorchedSammy 62a6cc56b9
docs: document hilbish.highlighter 2022-03-26 18:28:01 -04:00
TorchedSammy e5d841a0a7 docs: [ci] generate new docs 2022-03-26 22:26:10 +00:00
TorchedSammy 3e50e608c1
chore: merge from remote 2022-03-26 18:25:35 -04:00
TorchedSammy 76f100ca77
feat: expose syntax highlighting (closes #125) 2022-03-26 18:25:19 -04:00
TorchedSammy 0cad0e7e66
fix: only try to run hinter function if it isnt nil 2022-03-26 18:24:49 -04:00
TorchedSammy 2fb481c4cb docs: [ci] generate new docs 2022-03-26 21:34:42 +00:00
TorchedSammy 6ea25a22b3
feat: add inline hint text and change what were hints previously to info (closes #126) 2022-03-26 17:34:09 -04:00
TorchedSammy 577f00dfef
fix(readline): make forward delete work properly 2022-03-23 21:11:24 -04:00
TorchedSammy 722bd1cd80
fix(readline): insert text in replace mode if cursor is at end of text 2022-03-23 21:10:04 -04:00
TorchedSammy 1ba314d961
chore: prepare for v1.2.0 release 2022-03-22 22:11:46 -04:00
sammyette 84dce8c537
docs: change short description back to block quote 2022-03-22 22:04:33 -04:00
TorchedSammy 3345c51064
docs: add newline after short description 2022-03-22 22:01:22 -04:00
TorchedSammy a7e450904c
docs: add more info to readme 2022-03-22 21:59:34 -04:00
TorchedSammy 23efc8e54d
docs: update image for readme gallery 2022-03-22 21:19:36 -04:00
TorchedSammy 1d4c8a7645
feat: make history clear function work again 2022-03-22 19:01:29 -04:00
TorchedSammy 7272e035d9
ci: remove fetch-depth 0 setting for checkout 2022-03-22 18:41:26 -04:00
TorchedSammy 8a215ad742
docs: rename vimMode to vim-mode 2022-03-22 18:39:22 -04:00
TorchedSammy 6e69ee20f6
chore: merge 2022-03-22 18:38:26 -04:00
TorchedSammy bc15da2f1a
docs: add more docs for runner mode interface 2022-03-22 18:38:13 -04:00
TorchedSammy dd9e827735 docs: [ci] generate new docs 2022-03-22 22:33:52 +00:00
TorchedSammy 3636efe7f8
docs: add doc for mode param of runnerMode function 2022-03-22 18:33:11 -04:00
TorchedSammy 053914ec45
docs: fix dates for changelog versions 2022-03-22 17:19:23 -04:00
TorchedSammy 1e884e7c89
fix: handle job being nil (first sh exec case) 2022-03-21 21:25:43 -04:00
TorchedSammy f27d60f827
fix: move cursor to end of line on history search (closes #121) 2022-03-21 06:47:14 -04:00
TorchedSammy 754a63c74b
chore: merge 2022-03-20 19:10:41 -04:00
TorchedSammy 2fe888e186
feat: add hilbish.jobs interface and add stop function to job in hooks (closes #109) 2022-03-20 19:10:12 -04:00
TorchedSammy 0d4143582f docs: [ci] generate new docs 2022-03-20 21:54:55 +00:00
TorchedSammy 654ca4b527
docs: fix hilbish.alias doc 2022-03-20 17:54:02 -04:00
TorchedSammy 802f444ba6 docs: [ci] generate new docs 2022-03-20 19:16:13 +00:00
TorchedSammy 86a15e6363
feat: add configurable runner mode (closes #110) 2022-03-20 15:15:44 -04:00
TorchedSammy 96c1487bfa
fix: make sure complete input is added to history 2022-03-19 18:48:03 -04:00
TorchedSammy 1e899bf18e
chore: set name of history in menu to History instead of file 2022-03-19 13:24:12 -04:00
TorchedSammy f03f8c0da1
docs: add exitCode to job docs 2022-03-19 13:14:12 -04:00
TorchedSammy 1378a74e87
feat: add job hooks (part of #109) 2022-03-19 13:10:50 -04:00
TorchedSammy 63bc398f1c
fix: use unexported alias handler init function 2022-03-19 12:44:26 -04:00
TorchedSammy 579a0cd0ce
refactor: rename hilbishAliases to aliasHandler for clarity 2022-03-19 12:43:48 -04:00
TorchedSammy f433ab8a6f
docs(guide): mention that users can copy the default dir from dataDir 2022-03-19 09:50:51 -04:00
TorchedSammy eb0a81f7a2
chore: prepare for v1.1.0 release 2022-03-17 20:56:19 -04:00
TorchedSammy 24b88a0483
docs: add docs for vim mode 2022-03-17 20:25:38 -04:00
TorchedSammy f73c6d4aa8
fix: completions of executables and running absolute paths on windows 2022-03-17 20:22:30 -04:00
TorchedSammy 925ded6cea
fix(readline): remove duplicate code 2022-03-17 19:57:57 -04:00
TorchedSammy 92d0e195ab
fix: change prompt back to user's prompt on contine prompt exit 2022-03-17 19:49:33 -04:00
TorchedSammy 4da82e872c
fix: completions on files/folders starting with a dot not having it 2022-03-17 19:41:37 -04:00
TorchedSammy b0ece71de3
fix: use not-executable in prelude instead of no-perm 2022-03-17 19:29:27 -04:00
TorchedSammy 8b5dc69950
feat: add command.not-executable hook (closes #119) 2022-03-16 19:45:55 -04:00
sammyette 20a4cdb505
fix: handle path binaries properly on windows (closes #117, #118) (#120)
* fix: handle path binaries properly on windows (closes #117, #118)

* refactor: dont return exec name since it isnt needed

* fix: return correct error in find exec function and stat always

* fix: remove filepath import for exec file check on unix
2022-03-16 19:44:32 -04:00
TorchedSammy 01d937afd8
fix: correct username in greeting on windows 2022-03-16 18:42:38 -04:00
sammyette 32b421d402
docs: remove support notice 2022-03-15 22:20:23 -04:00
TorchedSammy 0ee47cc6f0
fix(readline): clear history filter on Readline, fixes filtering after ctrl-c 2022-03-15 16:27:12 -04:00
65 changed files with 2655 additions and 929 deletions

View File

@ -12,16 +12,14 @@ jobs:
matrix:
goos: [linux, windows, darwin]
goarch: ["386", amd64, arm64]
exclude:
exclude:
- goarch: "386"
goos: darwin
goos: darwin
- goarch: arm64
goos: windows
steps:
- name: Checkout sources
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v2
with:

View File

@ -1,25 +1,56 @@
# 🎀 Changelog
## [1.0.4] - 2021-03-12
## [1.2.0] - 2022-03-17
### Added
- Job Management additions
- `job.start` and `job.done` hooks (`doc hooks job`)
- `hilbish.jobs` interface (`get(id)` function gets a job object via `id`, `all()` gets all)
- Customizable runner/exec mode
- However Hilbish runs interactive user input can now be changed Lua side (`doc runner-mode`)
### Changed
- `vimMode` doc is now `vim-mode`
### Fixed
- Make sure input which is supposed to go in history goes there
- Cursor is right at the end of input on history search
## [1.1.0] - 2022-03-17
### Added
- `hilbish.vimAction` hook (`doc vimMode actions`)
- `command.not-executable` hook (will replace `command.no-perm` in a future release)
### Fixed
- Check if interactive before adding to history
- Escape in vim mode exits all modes and not only insert
- Make 2nd line in prompt empty if entire prompt is 1 line
- Completion menu doesnt appear if there is only 1 result
- Ignore SIGQUIT, which caused a panic unhandled
- Remove hostname in greeting on Windows
- Handle PATH binaries properly on Windows
- Fix removal of dot in the beginning of folders/files that have them for file complete
- Fix prompt being set to the continue prompt even when exited
## [1.0.4] - 2022-03-12
### Fixed
- Panic when history directory doesn't exist
## [1.0.3] - 2021-03-12
## [1.0.3] - 2022-03-12
### Fixed
- Removed duplicate executable suggestions
- User input is added to history now instead of what's ran by Hilbish
- Formatting issue with prompt on no input
## [1.0.2] - 2021-03-06
## [1.0.2] - 2022-03-06
### Fixed
- Cases where Hilbish's history directory doesn't exist will no longer cause a panic
## [1.0.1] - 2021-03-06
## [1.0.1] - 2022-03-06
### Fixed
- Using `hilbish.appendPath` will no longer result in string spam (debugging thing left being)
- Prompt gets set properly on startup
## [1.0.0] - 2021-03-06
## [1.0.0] - 2022-03-06
### Added
- MacOS is now officialy supported, default compile time vars have been added
for it
@ -392,6 +423,7 @@ This input for example will prompt for more input to complete:
First "stable" release of Hilbish.
[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
[1.0.2]: https://github.com/Rosettea/Hilbish/compare/v1.0.1...v1.0.2

View File

@ -1,31 +1,30 @@
PREFIX ?= /usr
DESTDIR ?=
BINDIR ?= $(PREFIX)/bin
LIBDIR ?= $(PREFIX)/share/hilbish
build:
@go build -ldflags "-s -w"
MY_GOFLAGS = -ldflags "-s -w"
dev:
@go build -ldflags "-s -w -X main.version=$(shell git describe --tags)"
all: dev
dev: MY_GOFLAGS = -ldflags "-s -w -X main.version=$(shell git describe --tags)"
dev: build
build:
go build $(MY_GOFLAGS)
install:
@install -v -d "$(DESTDIR)$(BINDIR)/" && install -m 0755 -v hilbish "$(DESTDIR)$(BINDIR)/hilbish"
@mkdir -p "$(DESTDIR)$(LIBDIR)"
@cp libs docs emmyLuaDocs prelude .hilbishrc.lua "$(DESTDIR)$(LIBDIR)" -r
@grep "$(DESTDIR)$(BINDIR)/hilbish" -qxF /etc/shells || echo "$(DESTDIR)$(BINDIR)/hilbish" >> /etc/shells
@echo "Hilbish Installed"
install -v -d "$(DESTDIR)$(BINDIR)/" && install -m 0755 -v hilbish "$(DESTDIR)$(BINDIR)/hilbish"
mkdir -p "$(DESTDIR)$(LIBDIR)"
cp -r libs docs emmyLuaDocs prelude .hilbishrc.lua "$(DESTDIR)$(LIBDIR)"
grep -qxF "$(DESTDIR)$(BINDIR)/hilbish" /etc/shells || echo "$(DESTDIR)$(BINDIR)/hilbish" >> /etc/shells
uninstall:
@rm -vrf \
rm -vrf \
"$(DESTDIR)$(BINDIR)/hilbish" \
"$(DESTDIR)$(LIBDIR)"
@sed -i '/hilbish/d' /etc/shells
@echo "Hilbish Uninstalled"
sed -i '/hilbish/d' /etc/shells
clean:
@go clean
go clean
all: build install
.PHONY: install uninstall build dev clean
.PHONY: all dev build install uninstall clean

View File

@ -2,7 +2,7 @@
<img src="./assets/hilbish-flower.png" width=128><br>
<img src="./assets/hilbish-text.png" width=256><br>
<blockquote>
🌺 The flower shell. A comfy and nice little shell for Lua users and fans!
🌺 The flower shell. A comfy and nice little shell for Lua fans!
</blockquote>
<p align="center">
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/Rosettea/Hilbish?style=flat-square">
@ -14,12 +14,25 @@
</p>
</div>
Hilbish is a Unix-y shell which uses Lua for scripting. Things like the prompt,
general configuration and such are done with Lua.
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.
For interactive use, it uses a library to run sh which works on all
platforms Hilbish can be compiled for. It can also act as a Lua REPL if you want
it to be.
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
<div align="center">
@ -29,8 +42,6 @@ it to be.
</div>
# Installation
**NOTE:** Hilbish is currently only officially supported and tested on Linux
## Prebuilt binaries
Go [here](https://nightly.link/Rosettea/Hilbish/workflows/build/master) for
builds on the master branch.
@ -56,7 +67,7 @@ If you're new to nix you should probably read up on how to do that [here](https:
### Prerequisites
- [Go 1.17+](https://go.dev)
#### Build
### Build
First, clone Hilbish. The recursive is required, as some Lua libraries
are submodules.
```sh
@ -78,13 +89,27 @@ make build
After you did all that, run `sudo make install` to install Hilbish globally.
# Getting Started
At startup, you should see a message which says to run a `guide` command.
This guide is a *very* simple and basic step through text of what Hilbish is
and where to find documentation.
Documentation is primarily viewed via the in shell `doc` command.
Autogenerated function docs and general docs about other things are included
there, so be sure to read it.
Using Hilbish is the same as using any other Linux shell, with an addition
that you can also run Lua. Hilbish can also act as an enhanced Lua REPL
via `hilbish.runnerMode 'lua'`. To switch back to normal, use
`hilbish.runnerMode 'hybrid'`.
# Contributing
Any kind of contributions to Hilbish are welcome!
Read [CONTRIBUTING.md](CONTRIBUTING.md) before getting started.
Any kind of contributions are welcome! Hilbish is very easy to contribute to.
Read [CONTRIBUTING.md](CONTRIBUTING.md) as a guideline to doing so.
**Thanks to everyone below who's contributed!**
<a href="https://github.com/Hilbis/Hilbish/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Hilbis/Hilbish" />
<a href="https://github.com/Rosettea/Hilbish/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Rosettea/Hilbish" />
</a>
*Made with [contributors-img](https://contrib.rocks).*

View File

@ -4,57 +4,59 @@ import (
"strings"
"sync"
"github.com/yuin/gopher-lua"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
)
var aliases *hilbishAliases
var aliases *aliasHandler
type hilbishAliases struct {
type aliasHandler struct {
aliases map[string]string
mu *sync.RWMutex
}
// initialize aliases map
func NewAliases() *hilbishAliases {
return &hilbishAliases{
func newAliases() *aliasHandler {
return &aliasHandler{
aliases: make(map[string]string),
mu: &sync.RWMutex{},
}
}
func (h *hilbishAliases) Add(alias, cmd string) {
h.mu.Lock()
defer h.mu.Unlock()
func (a *aliasHandler) Add(alias, cmd string) {
a.mu.Lock()
defer a.mu.Unlock()
h.aliases[alias] = cmd
a.aliases[alias] = cmd
}
func (h *hilbishAliases) All() map[string]string {
return h.aliases
func (a *aliasHandler) All() map[string]string {
return a.aliases
}
func (h *hilbishAliases) Delete(alias string) {
h.mu.Lock()
defer h.mu.Unlock()
func (a *aliasHandler) Delete(alias string) {
a.mu.Lock()
defer a.mu.Unlock()
delete(h.aliases, alias)
delete(a.aliases, alias)
}
func (h *hilbishAliases) Resolve(cmdstr string) string {
h.mu.RLock()
defer h.mu.RUnlock()
func (a *aliasHandler) Resolve(cmdstr string) string {
a.mu.RLock()
defer a.mu.RUnlock()
args := strings.Split(cmdstr, " ")
for h.aliases[args[0]] != "" {
alias := h.aliases[args[0]]
for a.aliases[args[0]] != "" {
alias := a.aliases[args[0]]
cmdstr = alias + strings.TrimPrefix(cmdstr, args[0])
cmdArgs, _ := splitInput(cmdstr)
args = cmdArgs
if h.aliases[args[0]] == alias {
if a.aliases[args[0]] == alias {
break
}
if h.aliases[args[0]] != "" {
if a.aliases[args[0]] != "" {
continue
}
}
@ -64,41 +66,38 @@ func (h *hilbishAliases) Resolve(cmdstr string) string {
// lua section
func (h *hilbishAliases) Loader(L *lua.LState) *lua.LTable {
func (a *aliasHandler) Loader(rtm *rt.Runtime) *rt.Table {
// create a lua module with our functions
hshaliasesLua := map[string]lua.LGFunction{
"add": h.luaAdd,
"list": h.luaList,
"del": h.luaDelete,
hshaliasesLua := map[string]util.LuaExport{
"add": util.LuaExport{hlalias, 2, false},
"list": util.LuaExport{a.luaList, 0, false},
"del": util.LuaExport{a.luaDelete, 1, false},
}
mod := L.SetFuncs(L.NewTable(), hshaliasesLua)
mod := rt.NewTable()
util.SetExports(rtm, mod, hshaliasesLua)
return mod
}
func (h *hilbishAliases) luaAdd(L *lua.LState) int {
alias := L.CheckString(1)
cmd := L.CheckString(2)
h.Add(alias, cmd)
return 0
}
func (h *hilbishAliases) luaList(L *lua.LState) int {
aliasesList := L.NewTable()
for k, v := range h.All() {
aliasesList.RawSetString(k, lua.LString(v))
func (a *aliasHandler) 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))
}
L.Push(aliasesList)
return 1
return c.PushingNext1(t.Runtime, rt.TableValue(aliasesList)), nil
}
func (h *hilbishAliases) luaDelete(L *lua.LState) int {
alias := L.CheckString(1)
h.Delete(alias)
func (a *aliasHandler) luaDelete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
alias, err := c.StringArg(0)
if err != nil {
return nil, err
}
a.Delete(alias)
return 0
return c.Next(), nil
}

718
api.go
View File

@ -4,6 +4,8 @@
package main
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
@ -14,182 +16,128 @@ import (
"hilbish/util"
"github.com/yuin/gopher-lua"
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib"
"github.com/maxlandon/readline"
"github.com/blackfireio/osinfo"
"mvdan.cc/sh/v3/interp"
)
var exports = map[string]lua.LGFunction {
"alias": hlalias,
"appendPath": hlappendPath,
"complete": hlcomplete,
"cwd": hlcwd,
"exec": hlexec,
"goro": hlgoro,
"multiprompt": hlmlprompt,
"prependPath": hlprependPath,
"prompt": hlprompt,
"inputMode": hlinputMode,
"interval": hlinterval,
"read": hlread,
"run": hlrun,
"timeout": hltimeout,
"which": hlwhich,
var exports = map[string]util.LuaExport{
"alias": {hlalias, 2, false},
"appendPath": {hlappendPath, 1, false},
"complete": {hlcomplete, 2, false},
"cwd": {hlcwd, 0, false},
"exec": {hlexec, 1, false},
"runnerMode": {hlrunnerMode, 1, false},
"goro": {hlgoro, 1, true},
"highlighter": {hlhighlighter, 1, false},
"hinter": {hlhinter, 1, false},
"multiprompt": {hlmultiprompt, 1, false},
"prependPath": {hlprependPath, 1, false},
"prompt": {hlprompt, 1, true},
"inputMode": {hlinputMode, 1, false},
"interval": {hlinterval, 2, false},
"read": {hlread, 1, false},
"run": {hlrun, 1, true},
"timeout": {hltimeout, 2, false},
"which": {hlwhich, 1, false},
}
var greeting string
var hshMod *lua.LTable
var hshMod *rt.Table
var hilbishLoader = packagelib.Loader{
Load: hilbishLoad,
Name: "hilbish",
}
func hilbishLoader(L *lua.LState) int {
mod := L.SetFuncs(L.NewTable(), exports)
func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) {
mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
hshMod = mod
host, _ := os.Hostname()
username := curuser.Username
greeting = `Welcome to {magenta}Hilbish{reset}, {cyan}` + curuser.Username + `{reset}.
The nice lil shell for {blue}Lua{reset} fanatics!
Check out the {blue}{bold}guide{reset} command to get started.
`
if runtime.GOOS == "windows" {
username = strings.Split(username, "\\")[1] // for some reason Username includes the hostname on windows
}
util.SetField(L, mod, "ver", lua.LString(version), "Hilbish version")
util.SetField(L, mod, "user", lua.LString(username), "Username of user")
util.SetField(L, mod, "host", lua.LString(host), "Host name of the machine")
util.SetField(L, mod, "home", lua.LString(curuser.HomeDir), "Home directory of the user")
util.SetField(L, mod, "dataDir", lua.LString(dataDir), "Directory for Hilbish's data files")
util.SetField(L, mod, "interactive", lua.LBool(interactive), "If this is an interactive shell")
util.SetField(L, mod, "login", lua.LBool(interactive), "Whether this is a login shell")
util.SetField(L, mod, "greeting", lua.LString(greeting), "Hilbish's welcome message for interactive shells. It has Lunacolors formatting.")
util.SetField(l, mod, "vimMode", lua.LNil, "Current Vim mode of Hilbish (nil if not in Vim mode)")
util.SetField(l, hshMod, "exitCode", lua.LNumber(0), "Exit code of last exected command")
util.Document(L, mod, "Hilbish's core API, containing submodules and functions which relate to the shell itself.")
greeting = `Welcome to {magenta}Hilbish{reset}, {cyan}` + username + `{reset}.
The nice lil shell for {blue}Lua{reset} fanatics!
Check out the {blue}{bold}guide{reset} command to get started.
`
util.SetField(rtm, mod, "ver", rt.StringValue(version), "Hilbish version")
util.SetField(rtm, mod, "user", rt.StringValue(username), "Username of user")
util.SetField(rtm, mod, "host", rt.StringValue(host), "Host name of the machine")
util.SetField(rtm, mod, "home", rt.StringValue(curuser.HomeDir), "Home directory of the user")
util.SetField(rtm, mod, "dataDir", rt.StringValue(dataDir), "Directory for Hilbish's data files")
util.SetField(rtm, mod, "interactive", rt.BoolValue(interactive), "If this is an interactive shell")
util.SetField(rtm, mod, "login", rt.BoolValue(login), "Whether this is a login shell")
util.SetField(rtm, mod, "greeting", rt.StringValue(greeting), "Hilbish's welcome message for interactive shells. It has Lunacolors formatting.")
util.SetField(rtm, mod, "vimMode", rt.NilValue, "Current Vim mode of Hilbish (nil if not in Vim mode)")
util.SetField(rtm, hshMod, "exitCode", rt.IntValue(0), "Exit code of last exected command")
util.Document(mod, "Hilbish's core API, containing submodules and functions which relate to the shell itself.")
// hilbish.userDir table
hshuser := L.NewTable()
hshuser := rt.NewTable()
util.SetField(L, hshuser, "config", lua.LString(confDir), "User's config directory")
util.SetField(L, hshuser, "data", lua.LString(userDataDir), "XDG data directory")
util.Document(L, hshuser, "User directories to store configs and/or modules.")
L.SetField(mod, "userDir", hshuser)
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.")
mod.Set(rt.StringValue("userDir"), rt.TableValue(hshuser))
// hilbish.os table
hshos := L.NewTable()
hshos := rt.NewTable()
info, _ := osinfo.GetOSInfo()
util.SetField(L, hshos, "family", lua.LString(info.Family), "Family name of the current OS")
util.SetField(L, hshos, "name", lua.LString(info.Name), "Pretty name of the current OS")
util.SetField(L, hshos, "version", lua.LString(info.Version), "Version of the current OS")
util.Document(L, hshos, "OS info interface")
L.SetField(mod, "os", hshos)
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")
mod.Set(rt.StringValue("os"), rt.TableValue(hshos))
// hilbish.aliases table
aliases = NewAliases()
aliasesModule := aliases.Loader(L)
util.Document(L, aliasesModule, "Alias inferface for Hilbish.")
L.SetField(mod, "aliases", aliasesModule)
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(L)
util.Document(L, historyModule, "History interface for Hilbish.")
L.SetField(mod, "history", historyModule)
historyModule := lr.Loader(rtm)
mod.Set(rt.StringValue("history"), rt.TableValue(historyModule))
util.Document(historyModule, "History interface for Hilbish.")
// hilbish.completions table
hshcomp := L.NewTable()
// hilbish.completion table
hshcomp := rt.NewTable()
util.SetField(rtm, hshcomp, "files",
rt.FunctionValue(rt.NewGoFunction(luaFileComplete, "files", 3, false)),
"Completer for files")
util.SetField(L, hshcomp, "files", L.NewFunction(luaFileComplete), "Completer for files")
util.SetField(L, hshcomp, "bins", L.NewFunction(luaBinaryComplete), "Completer for executables/binaries")
util.Document(L, hshcomp, "Completions interface for Hilbish.")
L.SetField(mod, "completion", hshcomp)
util.SetField(rtm, hshcomp, "bins",
rt.FunctionValue(rt.NewGoFunction(luaBinaryComplete, "bins", 3, false)),
"Completer for executables/binaries")
L.Push(mod)
util.Document(hshcomp, "Completions interface for Hilbish.")
mod.Set(rt.StringValue("completion"), rt.TableValue(hshcomp))
return 1
}
// hilbish.runner table
runnerModule := runnerModeLoader(rtm)
util.Document(runnerModule, "Runner/exec interface for Hilbish.")
mod.Set(rt.StringValue("runner"), rt.TableValue(runnerModule))
func luaFileComplete(L *lua.LState) int {
query := L.CheckString(1)
ctx := L.CheckString(2)
fields := L.CheckTable(3)
// hilbish.jobs table
jobs = newJobHandler()
jobModule := jobs.loader(rtm)
util.Document(jobModule, "(Background) job interface.")
mod.Set(rt.StringValue("jobs"), rt.TableValue(jobModule))
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))
var fds []string
fields.ForEach(func(k lua.LValue, v lua.LValue) {
fds = append(fds, v.String())
})
completions := fileComplete(query, ctx, fds)
luaComps := L.NewTable()
for _, comp := range completions {
luaComps.Append(lua.LString(comp))
}
L.Push(luaComps)
return 1
}
func luaBinaryComplete(L *lua.LState) int {
query := L.CheckString(1)
ctx := L.CheckString(2)
fields := L.CheckTable(3)
var fds []string
fields.ForEach(func(k lua.LValue, v lua.LValue) {
fds = append(fds, v.String())
})
completions, _ := binaryComplete(query, ctx, fds)
luaComps := L.NewTable()
for _, comp := range completions {
luaComps.Append(lua.LString(comp))
}
L.Push(luaComps)
return 1
}
func setVimMode(mode string) {
util.SetField(l, hshMod, "vimMode", lua.LString(mode), "Current Vim mode of Hilbish (nil if not in Vim mode)")
hooks.Em.Emit("hilbish.vimMode", mode)
}
func unsetVimMode() {
util.SetField(l, hshMod, "vimMode", lua.LNil, "Current Vim mode of Hilbish (nil if not in Vim mode)")
}
// run(cmd)
// Runs `cmd` in Hilbish's sh interpreter.
// --- @param cmd string
func hlrun(L *lua.LState) int {
var exitcode uint8
cmd := L.CheckString(1)
err := execCommand(cmd, cmd)
if code, ok := interp.IsExitStatus(err); ok {
exitcode = code
} else if err != nil {
exitcode = 1
}
L.Push(lua.LNumber(exitcode))
return 1
}
// cwd()
// Returns the current directory of the shell
func hlcwd(L *lua.LState) int {
cwd, _ := os.Getwd()
L.Push(lua.LString(cwd))
return 1
return rt.TableValue(mod), nil
}
func getenv(key, fallback string) string {
@ -200,28 +148,164 @@ func getenv(key, fallback string) string {
return value
}
func luaFileComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
query, ctx, fds, err := getCompleteParams(t, c)
if err != nil {
return nil, err
}
completions, _ := fileComplete(query, ctx, fds)
luaComps := rt.NewTable()
for i, comp := range completions {
luaComps.Set(rt.IntValue(int64(i + 1)), rt.StringValue(comp))
}
return c.PushingNext1(t.Runtime, rt.TableValue(luaComps)), 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
}
completions, _ := binaryComplete(query, ctx, fds)
luaComps := rt.NewTable()
for i, comp := range completions {
luaComps.Set(rt.IntValue(int64(i + 1)), rt.StringValue(comp))
}
return c.PushingNext1(t.Runtime, rt.TableValue(luaComps)), nil
}
func getCompleteParams(t *rt.Thread, c *rt.GoCont) (string, string, []string, error) {
if err := c.CheckNArgs(3); err != nil {
return "", "", []string{}, err
}
query, err := c.StringArg(0)
if err != nil {
return "", "", []string{}, err
}
ctx, err := c.StringArg(1)
if err != nil {
return "", "", []string{}, err
}
fields, err := c.TableArg(2)
if err != nil {
return "", "", []string{}, err
}
var fds []string
nextVal := rt.NilValue
for {
next, val, ok := fields.Next(nextVal)
if next == rt.NilValue {
break
}
nextVal = next
valStr, ok := val.TryString()
if !ok {
continue
}
fds = append(fds, valStr)
}
return query, ctx, fds, err
}
func setVimMode(mode string) {
util.SetField(l, hshMod, "vimMode", rt.StringValue(mode), "Current Vim mode of Hilbish (nil if not in Vim mode)")
hooks.Em.Emit("hilbish.vimMode", mode)
}
func unsetVimMode() {
util.SetField(l, hshMod, "vimMode", rt.NilValue, "Current Vim mode of Hilbish (nil if not in Vim mode)")
}
// 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 hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
var terminalOut bool
if len(c.Etc()) != 0 {
tout := c.Etc()[0]
termOut, ok := tout.TryBool()
terminalOut = termOut
if !ok {
return nil, errors.New("bad argument to run (expected boolean, got " + tout.TypeName() + ")")
}
} else {
terminalOut = true
}
var exitcode uint8
stdout, stderr, err := execCommand(cmd, terminalOut)
if code, ok := interp.IsExitStatus(err); ok {
exitcode = code
} else if err != nil {
exitcode = 1
}
stdoutStr := ""
stderrStr := ""
if !terminalOut {
stdoutStr = stdout.(*bytes.Buffer).String()
stderrStr = stderr.(*bytes.Buffer).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
func hlcwd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
cwd, _ := os.Getwd()
return c.PushingNext1(t.Runtime, rt.StringValue(cwd)), nil
}
// 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)
// --- @param prompt string
func hlread(L *lua.LState) int {
luaprompt := L.CheckString(1)
func hlread(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
luaprompt, err := c.StringArg(0)
if err != nil {
return nil, err
}
lualr := newLineReader("", true)
lualr.SetPrompt(luaprompt)
input, err := lualr.Read()
if err != nil {
L.Push(lua.LNil)
return 1
return c.Next(), nil
}
L.Push(lua.LString(input))
return 1
return c.PushingNext1(t.Runtime, rt.StringValue(input)), nil
}
/*
prompt(str)
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.
@ -229,53 +313,110 @@ These will be formatted and replaced with the appropriate values.
`%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.
*/
func hlprompt(L *lua.LState) int {
prompt = L.CheckString(1)
lr.SetPrompt(fmtPrompt(prompt))
func hlprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
err := c.Check1Arg()
if err != nil {
return nil, err
}
p, err := c.StringArg(0)
if err != nil {
return nil, err
}
typ := "left"
// optional 2nd arg
if len(c.Etc()) != 0 {
ltyp := c.Etc()[0]
var ok bool
typ, ok = ltyp.TryString()
if !ok {
return nil, errors.New("bad argument to run (expected string, got " + ltyp.TypeName() + ")")
}
}
return 0
switch typ {
case "left":
prompt = p
lr.SetPrompt(fmtPrompt(prompt))
case "right": lr.SetRightPrompt(fmtPrompt(p))
default: return nil, errors.New("expected prompt type to be right or left, got " + typ)
}
return c.Next(), nil
}
// multiprompt(str)
// Changes the continued line prompt to `str`
// --- @param str string
func hlmlprompt(L *lua.LState) int {
multilinePrompt = L.CheckString(1)
func hlmultiprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
prompt, err := c.StringArg(0)
if err != nil {
return nil, err
}
multilinePrompt = prompt
return 0
return c.Next(), nil
}
// alias(cmd, orig)
// Sets an alias of `orig` to `cmd`
// Sets an alias of `cmd` to `orig`
// --- @param cmd string
// --- @param orig string
func hlalias(L *lua.LState) int {
alias := L.CheckString(1)
source := L.CheckString(2)
func hlalias(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(2); err != nil {
return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
orig, err := c.StringArg(1)
if err != nil {
return nil, err
}
aliases.Add(alias, source)
aliases.Add(cmd, orig)
return 1
return c.Next(), nil
}
// appendPath(dir)
// Appends `dir` to $PATH
// --- @param dir string|table
func hlappendPath(L *lua.LState) int {
func hlappendPath(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
arg := c.Arg(0)
// check if dir is a table or a string
arg := L.Get(1)
if arg.Type() == lua.LTTable {
arg.(*lua.LTable).ForEach(func(k lua.LValue, v lua.LValue) {
appendPath(v.String())
})
} else if arg.Type() == lua.LTString {
appendPath(arg.String())
if arg.Type() == rt.TableType {
nextVal := rt.NilValue
for {
next, val, ok := arg.AsTable().Next(nextVal)
if next == rt.NilValue {
break
}
nextVal = next
valStr, ok := val.TryString()
if !ok {
continue
}
appendPath(valStr)
}
} else if arg.Type() == rt.StringType {
appendPath(arg.AsString())
} else {
L.RaiseError("bad argument to appendPath (expected string or table, got %v)", L.Get(1).Type().String())
return nil, errors.New("bad argument to appendPath (expected string or table, got " + arg.TypeName() + ")")
}
return 0
return c.Next(), nil
}
func appendPath(dir string) {
@ -291,8 +432,14 @@ func appendPath(dir string) {
// exec(cmd)
// Replaces running hilbish with `cmd`
// --- @param cmd string
func hlexec(L *lua.LState) int {
cmd := L.CheckString(1)
func hlexec(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
cmdArgs, _ := splitInput(cmd)
if runtime.GOOS != "windows" {
cmdPath, err := exec.LookPath(cmdArgs[0])
@ -314,88 +461,82 @@ func hlexec(L *lua.LState) int {
os.Exit(0)
}
return 0
return c.Next(), nil
}
// goro(fn)
// Puts `fn` in a goroutine
// --- @param fn function
func hlgoro(L *lua.LState) int {
fn := L.CheckFunction(1)
argnum := L.GetTop()
args := make([]lua.LValue, argnum)
for i := 1; i <= argnum; i++ {
args[i - 1] = L.Get(i)
func hlgoro(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
fn, err := c.ClosureArg(0)
if err != nil {
return nil, err
}
// call fn
go func() {
if err := L.CallByParam(lua.P{
Fn: fn,
NRet: 0,
Protect: true,
}, args...); err != nil {
_, err := rt.Call1(l.MainThread(), rt.FunctionValue(fn), c.Etc()...)
if err != nil {
fmt.Fprintln(os.Stderr, "Error in goro function:\n\n", err)
}
}()
return 0
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
func hltimeout(L *lua.LState) int {
cb := L.CheckFunction(1)
ms := L.CheckInt(2)
timeout := time.Duration(ms) * time.Millisecond
time.Sleep(timeout)
if err := L.CallByParam(lua.P{
Fn: cb,
NRet: 0,
Protect: true,
}); err != nil {
fmt.Fprintln(os.Stderr, "Error in goro function:\n\n", err)
// --- @return table
func hltimeout(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(2); err != nil {
return nil, err
}
return 0
cb, err := c.ClosureArg(0)
if err != nil {
return nil, err
}
ms, err := c.IntArg(1)
if err != nil {
return nil, err
}
interval := time.Duration(ms) * time.Millisecond
timer := timers.create(timerTimeout, interval, cb)
timer.start()
return c.PushingNext1(t.Runtime, timer.lua()), nil
}
// interval(cb, time)
// Runs the `cb` function every `time` milliseconds
// Runs the `cb` function every `time` milliseconds.
// Returns a `timer` object (see `doc timers`).
// --- @param cb function
// --- @param time number
func hlinterval(L *lua.LState) int {
intervalfunc := L.CheckFunction(1)
ms := L.CheckInt(2)
// --- @return table
func hlinterval(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(2); err != nil {
return nil, err
}
cb, err := c.ClosureArg(0)
if err != nil {
return nil, err
}
ms, err := c.IntArg(1)
if err != nil {
return nil, err
}
interval := time.Duration(ms) * time.Millisecond
timer := timers.create(timerInterval, interval, cb)
timer.start()
ticker := time.NewTicker(interval)
stop := make(chan lua.LValue)
go func() {
for {
select {
case <-ticker.C:
if err := L.CallByParam(lua.P{
Fn: intervalfunc,
NRet: 0,
Protect: true,
}); err != nil {
fmt.Fprintln(os.Stderr, "Error in interval function:\n\n", err)
stop <- lua.LTrue // stop the interval
}
case <-stop:
ticker.Stop()
return
}
}
}()
L.Push(lua.LChannel(stop))
return 1
return c.PushingNext1(t.Runtime, timer.lua()), nil
}
// complete(scope, cb)
@ -408,20 +549,27 @@ func hlinterval(L *lua.LState) int {
// `grid` (the normal file completion display) or `list` (with a description)
// --- @param scope string
// --- @param cb function
func hlcomplete(L *lua.LState) int {
scope := L.CheckString(1)
cb := L.CheckFunction(2)
func hlcomplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
scope, cb, err := util.HandleStrCallback(t, c)
if err != nil {
return nil, err
}
luaCompletions[scope] = cb
return 0
return c.Next(), nil
}
// prependPath(dir)
// Prepends `dir` to $PATH
// --- @param dir string
func hlprependPath(L *lua.LState) int {
dir := L.CheckString(1)
func hlprependPath(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 = strings.Replace(dir, "~", curuser.HomeDir, 1)
pathenv := os.Getenv("PATH")
@ -430,29 +578,40 @@ func hlprependPath(L *lua.LState) int {
os.Setenv("PATH", dir + string(os.PathListSeparator) + pathenv)
}
return 0
return c.Next(), nil
}
// which(binName)
// Searches for an executable called `binName` in the directories of $PATH
// --- @param binName string
func hlwhich(L *lua.LState) int {
binName := L.CheckString(1)
func hlwhich(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
binName, err := c.StringArg(0)
if err != nil {
return nil, err
}
path, err := exec.LookPath(binName)
if err != nil {
l.Push(lua.LNil)
return 1
return c.Next(), nil
}
l.Push(lua.LString(path))
return 1
return c.PushingNext1(t.Runtime, rt.StringValue(path)), nil
}
// inputMode(mode)
// Sets the input mode for Hilbish's line reader. Accepts either emacs for vim
// --- @param mode string
func hlinputMode(L *lua.LState) int {
mode := L.CheckString(1)
func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
mode, err := c.StringArg(0)
if err != nil {
return nil, err
}
switch mode {
case "emacs":
unsetVimMode()
@ -460,7 +619,74 @@ func hlinputMode(L *lua.LState) int {
case "vim":
setVimMode("insert")
lr.rl.InputMode = readline.Vim
default: L.RaiseError("inputMode: expected vim or emacs, received " + mode)
default:
return nil, errors.New("inputMode: expected vim or emacs, received " + mode)
}
return 0
return c.Next(), nil
}
// 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.
// --- @param mode string|function
func hlrunnerMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
mode := c.Arg(0)
switch mode.Type() {
case rt.StringType:
switch mode.AsString() {
// no fallthrough doesnt work so eh
case "hybrid", "hybridRev", "lua", "sh": runnerMode = mode
default: return nil, errors.New("execMode: expected either a function or hybrid, hybridRev, lua, sh. Received " + mode.AsString())
}
case rt.FunctionType: runnerMode = mode
default: return nil, errors.New("execMode: expected either a function or hybrid, hybridRev, lua, sh. Received " + mode.TypeName())
}
return c.Next(), nil
}
// hinter(cb)
// Sets the hinter function. This will be called on every key insert to determine
// what text to use as an inline hint. The callback is passed 2 arguments:
// the current line and the position. It is expected to return a string
// which will be used for the hint.
// --- @param cb function
func hlhinter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
hinterCb, err := c.ClosureArg(0)
if err != nil {
return nil, err
}
hinter = hinterCb
return c.Next(), err
}
// highlighter(cb)
// Sets the highlighter function. This is mainly for syntax hightlighting, but in
// reality could set the input of the prompt to display anything. The callback
// is passed the current line as typed and is expected to return a line that will
// be used to display in the line.
// --- @param cb function
func hlhighlighter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
highlighterCb, err := c.ClosureArg(0)
if err != nil {
return nil, err
}
highlighter = highlighterCb
return c.Next(), err
}

View File

@ -80,6 +80,9 @@ func main() {
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)
@ -111,6 +114,9 @@ func main() {
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)

View File

@ -2,27 +2,12 @@ package main
import (
"path/filepath"
"runtime"
"strings"
"os"
"unicode"
)
func fileComplete(query, ctx string, fields []string) []string {
var completions []string
prefixes := []string{"./", "../", "/", "~/"}
for _, prefix := range prefixes {
if strings.HasPrefix(query, prefix) {
completions, _ = matchPath(strings.Replace(query, "~", curuser.HomeDir, 1), query)
}
}
if len(completions) == 0 && len(fields) > 1 {
completions, _ = matchPath("./" + query, query)
}
return completions
func fileComplete(query, ctx string, fields []string) ([]string, string) {
return matchPath(query)
}
func binaryComplete(query, ctx string, fields []string) ([]string, string) {
@ -31,17 +16,17 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
prefixes := []string{"./", "../", "/", "~/"}
for _, prefix := range prefixes {
if strings.HasPrefix(query, prefix) {
fileCompletions := fileComplete(query, ctx, fields)
fileCompletions, filePref := matchPath(query)
if len(fileCompletions) != 0 {
for _, f := range fileCompletions {
name := strings.Replace(query + f, "~", curuser.HomeDir, 1)
if info, err := os.Stat(name); err == nil && info.Mode().Perm() & 0100 == 0 {
fullPath, _ := filepath.Abs(expandHome(query + strings.TrimPrefix(f, filePref)))
if err := findExecutable(fullPath, false, true); err != nil {
continue
}
completions = append(completions, f)
}
}
return completions, ""
return completions, filePref
}
}
@ -53,7 +38,8 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
// get basename from matches
for _, match := range matches {
// check if we have execute permissions for our match
if info, err := os.Stat(match); err == nil && info.Mode().Perm() & 0100 == 0 {
err := findExecutable(match, true, false)
if err != nil {
continue
}
// get basename from match
@ -76,55 +62,53 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
return completions, query
}
func matchPath(path, pref string) ([]string, error) {
func matchPath(query string) ([]string, string) {
var entries []string
matches, err := filepath.Glob(desensitize(path) + "*")
if err == nil {
args := []string{
"\"", "\\\"",
"'", "\\'",
"`", "\\`",
" ", "\\ ",
"(", "\\(",
")", "\\)",
"[", "\\[",
"]", "\\]",
}
var baseName string
r := strings.NewReplacer(args...)
for _, match := range matches {
name := filepath.Base(match)
p := filepath.Base(pref)
if pref == "" {
p = ""
path, _ := filepath.Abs(expandHome(filepath.Dir(query)))
if string(query) == "" {
// filepath base below would give us "."
// which would cause a match of only dotfiles
path, _ = filepath.Abs(".")
} else if !strings.HasSuffix(query, string(os.PathSeparator)) {
baseName = filepath.Base(query)
}
files, _ := os.ReadDir(path)
for _, file := range files {
if strings.HasPrefix(strings.ToLower(file.Name()), strings.ToLower(baseName)) {
entry := file.Name()
if file.IsDir() {
entry = entry + string(os.PathSeparator)
}
name = strings.TrimPrefix(name, p)
matchFull, _ := filepath.Abs(match)
if info, err := os.Stat(matchFull); err == nil && info.IsDir() {
name = name + string(os.PathSeparator)
}
name = r.Replace(name)
entries = append(entries, name)
entry = escapeFilename(entry)
entries = append(entries, entry)
}
}
return entries, err
return entries, baseName
}
func desensitize(text string) string {
if runtime.GOOS == "windows" {
return text
func escapeFilename(fname string) string {
args := []string{
"\"", "\\\"",
"'", "\\'",
"`", "\\`",
" ", "\\ ",
"(", "\\(",
")", "\\)",
"[", "\\[",
"]", "\\]",
"$", "\\$",
"&", "\\&",
"*", "\\*",
">", "\\>",
"<", "\\<",
"|", "\\|",
}
p := strings.Builder{}
for _, r := range text {
if unicode.IsLetter(r) {
p.WriteString("[" + string(unicode.ToLower(r)) + string(unicode.ToUpper(r)) + "]")
} else {
p.WriteString(string(r))
}
}
return p.String()
r := strings.NewReplacer(args...)
return r.Replace(fname)
}

View File

@ -1,4 +1,4 @@
alias(cmd, orig) > Sets an alias of `orig` to `cmd`
alias(cmd, orig) > Sets an alias of `cmd` to `orig`
appendPath(dir) > Appends `dir` to $PATH
@ -16,15 +16,26 @@ exec(cmd) > Replaces running hilbish with `cmd`
goro(fn) > Puts `fn` in a goroutine
highlighter(cb) > Sets the highlighter function. This is mainly for syntax hightlighting, but in
reality could set the input of the prompt to display anything. The callback
is passed the current line as typed and is expected to return a line that will
be used to display in the line.
hinter(cb) > Sets the hinter function. This will be called on every key insert to determine
what text to use as an inline hint. The callback is passed 2 arguments:
the current line and the position. It is expected to return a string
which will be used for the hint.
inputMode(mode) > Sets the input mode for Hilbish's line reader. Accepts either emacs for vim
interval(cb, time) > Runs the `cb` function every `time` milliseconds
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) > Changes the shell prompt to `str`
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
@ -35,9 +46,18 @@ read(prompt) -> input? > Read input from the user, using Hilbish's line editor/i
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) > Runs `cmd` in Hilbish's sh interpreter.
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(binName) > Searches for an executable called `binName` in the directories of $PATH

13
docs/hooks/job.txt 100644
View File

@ -0,0 +1,13 @@
Note: A `job` is a table with the following keys:
- cmd: command string
- running: boolean whether the job is running
- id: unique id for the job
- pid: process id for the job
- exitCode: exit code of the job
In ordinary cases you'd prefer to use the id instead of pid. The id is unique to
Hilbish and is how you get jobs with the `hilbish.jobs` interface.
+ `job.start` -> job > Thrown when a new background job starts.
+ `job.done` -> job > Thrown when a background jobs exits.

View File

@ -0,0 +1,42 @@
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 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

View File

@ -1,9 +1,9 @@
setRaw() > Puts the terminal in raw mode
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

30
docs/timers.txt 100644
View File

@ -0,0 +1,30 @@
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
Those previously mentioned functions return a `timer` object, to which you can
stop and start a timer again. The functions of the timers interface also
return a timer object.
## 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

View File

@ -0,0 +1,16 @@
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.
The `hilbish.vimAction` hook is thrown whenever a Vim action occurs.
It passes 2 arguments: the action name, and an array (table) of args
relating to it.
Here is documentation for what the table of args will hold for an
appropriate Vim action.
- `yank`: register, yankedText
The first argument for the yank action is the register yankedText goes to.
- `paste`: register, pastedText
The first argument for the paste action is the register pastedText is taken from.

View File

@ -0,0 +1,4 @@
Hilbish has a Vim binding input mode accessible for use.
It can be enabled with the `hilbish.inputMode` function (check `doc hilbish`).
This is documentation for everything relating to it.

View File

@ -15,6 +15,6 @@ function bait.catchOnce(name, cb) end
--- Throws a hook with `name` with the provided `args`
--- @param name string
--- @vararg any
function bait.throw(name) end
function bait.throw(name, ...) end
return bait

View File

@ -2,7 +2,7 @@
local hilbish = {}
--- Sets an alias of `orig` to `cmd`
--- Sets an alias of `cmd` to `orig`
--- @param cmd string
--- @param orig string
function hilbish.alias(cmd, orig) end
@ -33,18 +33,34 @@ function hilbish.exec(cmd) end
--- @param fn function
function hilbish.goro(fn) end
--- Sets the highlighter function. This is mainly for syntax hightlighting, but in
--- reality could set the input of the prompt to display anything. The callback
--- is passed the current line as typed and is expected to return a line that will
--- be used to display in the line.
--- @param cb function
function hilbish.highlighter(cb) end
--- Sets the hinter function. This will be called on every key insert to determine
--- what text to use as an inline hint. The callback is passed 2 arguments:
--- the current line and the position. It is expected to return a string
--- which will be used for the hint.
--- @param cb function
function hilbish.hinter(cb) end
--- Sets the input mode for Hilbish's line reader. Accepts either emacs for vim
--- @param mode string
function hilbish.inputMode(mode) end
--- Runs the `cb` function every `time` milliseconds
--- Runs the `cb` function every `time` milliseconds.
--- Returns a `timer` object (see `doc timers`).
--- @param cb function
--- @param time number
--- @return table
function hilbish.interval(cb, time) end
--- Changes the continued line prompt to `str`
--- @param str string
function hilbish.mlprompt(str) end
function hilbish.multiprompt(str) end
--- Prepends `dir` to $PATH
--- @param dir string
@ -57,7 +73,8 @@ function hilbish.prependPath(dir) end
--- `%u` - Name of current user
--- `%h` - Hostname of device
--- @param str string
function hilbish.prompt(str) end
--- @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.
@ -66,12 +83,24 @@ function hilbish.prompt(str) end
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
--- 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
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
function hilbish.timeout(cb, time) end
--- Searches for an executable called `binName` in the directories of $PATH

View File

@ -2,15 +2,15 @@
local terminal = {}
--- Puts the terminal in raw mode
function terminal.raw() end
--- Restores the last saved state of the terminal
function terminal.restoreState() end
--- Saves the current state of the terminal
function terminal.saveState() end
--- Puts the terminal in 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
function terminal.size() end

363
exec.go
View File

@ -1,30 +1,109 @@
package main
import (
"bytes"
"context"
"errors"
"os/exec"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"hilbish/util"
"github.com/yuin/gopher-lua"
rt "github.com/arnodel/golua/runtime"
"mvdan.cc/sh/v3/shell"
//"github.com/yuin/gopher-lua/parse"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
"mvdan.cc/sh/v3/expand"
)
func runInput(input, origInput string) {
var errNotExec = errors.New("not executable")
var runnerMode rt.Value = rt.StringValue("hybrid")
func runInput(input string, priv bool) {
running = true
cmdString := aliases.Resolve(input)
hooks.Em.Emit("command.preexec", input, cmdString)
var exitCode uint8
var err error
if runnerMode.Type() == rt.StringType {
switch runnerMode.AsString() {
case "hybrid":
_, _, err = handleLua(cmdString)
if err == nil {
cmdFinish(0, input, priv)
return
}
input, exitCode, err = handleSh(input)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
cmdFinish(exitCode, input, priv)
case "hybridRev":
_, _, err = handleSh(input)
if err == nil {
cmdFinish(0, input, priv)
return
}
input, exitCode, err = handleLua(cmdString)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
cmdFinish(exitCode, input, priv)
case "lua":
input, exitCode, err = handleLua(cmdString)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
cmdFinish(exitCode, input, priv)
case "sh":
input, exitCode, err = handleSh(input)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
cmdFinish(exitCode, input, priv)
}
} else {
// can only be a string or function so
term := rt.NewTerminationWith(l.MainThread().CurrentCont(), 2, false)
err := rt.Call(l.MainThread(), runnerMode, []rt.Value{rt.StringValue(cmdString)}, term)
if err != nil {
fmt.Fprintln(os.Stderr, err)
cmdFinish(124, input, priv)
return
}
luaexitcode := term.Get(0)
runErr := term.Get(1)
luaInput := term.Get(1)
var exitCode uint8
if code, ok := luaexitcode.TryInt(); ok {
exitCode = uint8(code)
}
if inp, ok := luaInput.TryString(); ok {
input = inp
}
if runErr != rt.NilValue {
fmt.Fprintln(os.Stderr, runErr)
}
cmdFinish(exitCode, input, priv)
}
}
func handleLua(cmdString string) (string, uint8, error) {
// First try to load input, essentially compiling to bytecode
fn, err := l.LoadString(cmdString)
chunk, err := l.CompileAndLoadLuaChunk("", []byte(cmdString), rt.TableValue(l.GlobalEnv()))
if err != nil && noexecute {
fmt.Println(err)
/* if lerr, ok := err.(*lua.ApiError); ok {
@ -33,62 +112,102 @@ func runInput(input, origInput string) {
}
}
*/
return
return cmdString, 125, err
}
// And if there's no syntax errors and -n isnt provided, run
if !noexecute {
l.Push(fn)
err = l.PCall(0, lua.MultRet, nil)
if chunk != nil {
_, err = rt.Call1(l.MainThread(), rt.FunctionValue(chunk))
}
}
if err == nil {
cmdFinish(0, cmdString, origInput)
return
return cmdString, 0, nil
}
// Last option: use sh interpreter
err = execCommand(cmdString, origInput)
return cmdString, 125, err
}
func handleSh(cmdString string) (string, uint8, error) {
_, _, err := execCommand(cmdString, true)
if err != nil {
// If input is incomplete, start multiline prompting
if syntax.IsIncomplete(err) {
if !interactive {
return cmdString, 126, err
}
for {
cmdString, err = continuePrompt(strings.TrimSuffix(cmdString, "\\"))
if err != nil {
break
}
err = execCommand(cmdString, origInput)
if syntax.IsIncomplete(err) || strings.HasSuffix(input, "\\") {
_, _, err = execCommand(cmdString, true)
if syntax.IsIncomplete(err) || strings.HasSuffix(cmdString, "\\") {
continue
} else if code, ok := interp.IsExitStatus(err); ok {
cmdFinish(code, cmdString, origInput)
return cmdString, code, nil
} else if err != nil {
fmt.Fprintln(os.Stderr, err)
cmdFinish(1, cmdString, origInput)
return cmdString, 126, err
} else {
cmdFinish(0, cmdString, origInput)
return cmdString, 0, nil
}
break
}
} else {
if code, ok := interp.IsExitStatus(err); ok {
cmdFinish(code, cmdString, origInput)
return cmdString, code, nil
} else {
cmdFinish(126, cmdString, origInput)
fmt.Fprintln(os.Stderr, err)
return cmdString, 126, err
}
}
} else {
cmdFinish(0, cmdString, origInput)
}
return cmdString, 0, nil
}
// Run command in sh interpreter
func execCommand(cmd, old string) error {
func execCommand(cmd string, terminalOut bool) (io.Writer, io.Writer, error) {
file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "")
if err != nil {
return err
return nil, nil, err
}
exechandle := func(ctx context.Context, args []string) 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)
}
buf := new(bytes.Buffer)
printer := syntax.NewPrinter()
var bg bool
for _, stmt := range file.Stmts {
bg = false
if stmt.Background {
bg = true
printer.Print(buf, stmt.Cmd)
stmtStr := buf.String()
buf.Reset()
jobs.add(stmtStr)
}
interp.ExecHandler(execHandle(bg))(runner)
err = runner.Run(context.TODO(), stmt)
if err != nil {
return stdout, stderr, err
}
}
return stdout, stderr, nil
}
func execHandle(bg bool) interp.ExecHandlerFunc {
return func(ctx context.Context, args []string) error {
_, argstring := splitInput(strings.Join(args, " "))
// i dont really like this but it works
if aliases.All()[args[0]] != "" {
@ -101,74 +220,176 @@ func execCommand(cmd, old string) error {
// If alias was found, use command alias
argstring = aliases.Resolve(argstring)
args, _ = shell.Fields(argstring, nil)
var err error
args, err = shell.Fields(argstring, nil)
if err != nil {
return err
}
}
// If command is defined in Lua then run it
luacmdArgs := l.NewTable()
for _, str := range args[1:] {
luacmdArgs.Append(lua.LString(str))
luacmdArgs := rt.NewTable()
for i, str := range args[1:] {
luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str))
}
if commands[args[0]] != nil {
err := l.CallByParam(lua.P{
Fn: commands[args[0]],
NRet: 1,
Protect: true,
}, luacmdArgs)
luaexitcode, err := rt.Call1(l.MainThread(), rt.FunctionValue(commands[args[0]]), rt.TableValue(luacmdArgs))
if err != nil {
fmt.Fprintln(os.Stderr,
"Error in command:\n\n" + err.Error())
fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error())
return interp.NewExitStatus(1)
}
luaexitcode := l.Get(-1)
var exitcode uint8
l.Pop(1)
if code, ok := luaexitcode.(lua.LNumber); luaexitcode != lua.LNil && ok {
if code, ok := luaexitcode.TryInt(); ok {
exitcode = uint8(code)
} else if luaexitcode != rt.NilValue {
// deregister commander
delete(commands, args[0])
fmt.Fprintf(os.Stderr, "Commander did not return number for exit code. %s, you're fired.\n", args[0])
}
cmdFinish(exitcode, argstring, old)
return interp.NewExitStatus(exitcode)
}
err := lookpath(args[0])
if err == os.ErrPermission {
if err == errNotExec {
hooks.Em.Emit("command.no-perm", args[0])
hooks.Em.Emit("command.not-executable", args[0])
return interp.NewExitStatus(126)
} else if err != nil {
hooks.Em.Emit("command.not-found", args[0])
return interp.NewExitStatus(127)
}
return interp.DefaultExecHandler(2 * time.Second)(ctx, args)
}
runner, _ := interp.New(
interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
interp.ExecHandler(exechandle),
)
err = runner.Run(context.TODO(), file)
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)
return interp.NewExitStatus(127)
}
return err
env := hc.Env
envList := make([]string, 0, 64)
env.Each(func(name string, vr expand.Variable) bool {
if !vr.IsSet() {
// If a variable is set globally but unset in the
// runner, we need to ensure it's not part of the final
// list. Seems like zeroing the element is enough.
// This is a linear search, but this scenario should be
// rare, and the number of variables shouldn't be large.
for i, kv := range envList {
if strings.HasPrefix(kv, name+"=") {
envList[i] = ""
}
}
}
if vr.Exported && vr.Kind == expand.String {
envList = append(envList, name+"="+vr.String())
}
return true
})
cmd := exec.Cmd{
Path: path,
Args: args,
Env: envList,
Dir: hc.Dir,
Stdin: hc.Stdin,
Stdout: hc.Stdout,
Stderr: hc.Stderr,
}
err = cmd.Start()
var j *job
if bg {
j = jobs.getLatest()
j.setHandle(cmd.Process)
}
if err == nil {
if bg {
j.start(cmd.Process.Pid)
}
if done := ctx.Done(); done != nil {
go func() {
<-done
if killTimeout <= 0 || runtime.GOOS == "windows" {
cmd.Process.Signal(os.Kill)
return
}
// TODO: don't temporarily leak this goroutine
// if the program stops itself with the
// interrupt.
go func() {
time.Sleep(killTimeout)
cmd.Process.Signal(os.Kill)
}()
cmd.Process.Signal(os.Interrupt)
}()
}
err = cmd.Wait()
}
var exit uint8
switch x := err.(type) {
case *exec.ExitError:
// started, but errored - default to 1 if OS
// doesn't have exit statuses
if status, ok := x.Sys().(syscall.WaitStatus); ok {
if status.Signaled() {
if ctx.Err() != nil {
return ctx.Err()
}
exit = uint8(128 + status.Signal())
goto end
}
exit = uint8(status.ExitStatus())
goto end
}
exit = 1
goto end
case *exec.Error:
// did not start
fmt.Fprintf(hc.Stderr, "%v\n", err)
exit = 127
goto end
case nil:
goto end
default:
return err
}
end:
if bg {
j.exitCode = int(exit)
j.finish()
}
return interp.NewExitStatus(exit)
}
}
// custom lookpath function so we know if a command is found *and* has execute permission
func lookpath(file string) error {
skip := []string{"./", "/", "../", "~/"}
func lookpath(file string) error { // custom lookpath function so we know if a command is found *and* is executable
var skip []string
if runtime.GOOS == "windows" {
skip = []string{"./", "../", "~/", "C:"}
} else {
skip = []string{"./", "/", "../", "~/"}
}
for _, s := range skip {
if strings.HasPrefix(file, s) {
err := findExecutable(file)
return err
return findExecutable(file, false, false)
}
}
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
path := filepath.Join(dir, file)
err := findExecutable(path)
if err == os.ErrPermission {
err := findExecutable(path, true, false)
if err == errNotExec {
return err
} else if err == nil {
return nil
@ -178,17 +399,6 @@ func lookpath(file string) error {
return os.ErrNotExist
}
func findExecutable(name string) error {
f, err := os.Stat(name)
if err != nil {
return err
}
if m := f.Mode(); !m.IsDir() && m & 0111 != 0 {
return nil
}
return os.ErrPermission
}
func splitInput(input string) ([]string, string) {
// end my suffering
// TODO: refactor this garbage
@ -242,11 +452,14 @@ func splitInput(input string) ([]string, string) {
return cmdArgs, cmdstr.String()
}
func cmdFinish(code uint8, cmdstr, oldInput string) {
func cmdFinish(code uint8, cmdstr string, private bool) {
// if input has space at the beginning, dont put in history
if interactive && !strings.HasPrefix(oldInput, " ") {
handleHistory(strings.TrimSpace(oldInput))
if interactive && !private {
handleHistory(cmdstr)
}
util.SetField(l, hshMod, "exitCode", lua.LNumber(code), "Exit code of last exected command")
hooks.Em.Emit("command.exit", code, cmdstr)
util.SetField(l, hshMod, "exitCode", rt.IntValue(int64(code)), "Exit code of last exected command")
// 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
hooks.Em.Emit("command.exit", rt.IntValue(int64(code)), cmdstr)
}

24
execfile_unix.go 100644
View File

@ -0,0 +1,24 @@
// +build linux darwin
package main
import (
"os"
)
func findExecutable(path string, inPath, dirs bool) error {
f, err := os.Stat(path)
if err != nil {
return err
}
if dirs {
if m := f.Mode(); m & 0111 != 0 {
return nil
}
} else {
if m := f.Mode(); !m.IsDir() && m & 0111 != 0 {
return nil
}
}
return errNotExec
}

View File

@ -0,0 +1,37 @@
// +build windows
package main
import (
"path/filepath"
"os"
)
func findExecutable(path string, inPath, dirs bool) error {
nameExt := filepath.Ext(path)
pathExts := filepath.SplitList(os.Getenv("PATHEXT"))
if inPath {
if nameExt == "" {
for _, ext := range pathExts {
_, err := os.Stat(path + ext)
if err == nil {
return nil
}
}
} else {
_, err := os.Stat(path)
if err == nil {
if contains(pathExts, nameExt) { return nil }
return errNotExec
}
}
} else {
_, err := os.Stat(path)
if err == nil {
if contains(pathExts, nameExt) { return nil }
return errNotExec
}
}
return os.ErrNotExist
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 99 KiB

19
go.mod
View File

@ -1,21 +1,32 @@
module hilbish
go 1.16
go 1.17
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/pborman/getopt v1.1.0
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
layeh.com/gopher-luar v1.0.10
mvdan.cc/sh/v3 v3.4.3
)
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/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
golang.org/x/text v0.3.6 // indirect
)
replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e
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-20220419183026-6d22d6fec5ac

41
go.sum
View File

@ -1,31 +1,24 @@
github.com/Rosettea/readline-1 v0.0.0-20220302012429-9ce5d23760f7 h1:LoY+kBKqMQqBcilRpVvifBTVve84asa3btpx3D/+IvM=
github.com/Rosettea/readline-1 v0.0.0-20220302012429-9ce5d23760f7/go.mod h1:QiUAvbhg8PzCA4hlafCUl0bKD/0VmcocM4AjqtszAJs=
github.com/Rosettea/readline-1 v0.0.0-20220305004552-071c22768119 h1:rGsc30WTD5hk+oiXrAKsAIwZn5qBeTAdr29y3HhJh9E=
github.com/Rosettea/readline-1 v0.0.0-20220305004552-071c22768119/go.mod h1:QiUAvbhg8PzCA4hlafCUl0bKD/0VmcocM4AjqtszAJs=
github.com/Rosettea/readline-1 v0.0.0-20220305123014-31d4d4214c93 h1:SmOkAEm3O7si8CURZSsSN0ZxCQ8IGiiulw8LMZ1V1Yc=
github.com/Rosettea/readline-1 v0.0.0-20220305123014-31d4d4214c93/go.mod h1:QiUAvbhg8PzCA4hlafCUl0bKD/0VmcocM4AjqtszAJs=
github.com/Rosettea/readline-1 v0.1.0-beta.0.20211207003625-341c7985ad7d h1:KBttN41h/tPahmpaZavviwQ8q4rCkt5CD0HdVmfgPVA=
github.com/Rosettea/readline-1 v0.1.0-beta.0.20211207003625-341c7985ad7d/go.mod h1:QiUAvbhg8PzCA4hlafCUl0bKD/0VmcocM4AjqtszAJs=
github.com/Rosettea/readline-1 v0.1.0-beta.0.20220228022904-61f5e4493011 h1:+a61iNamZiO3Xru+l/1qtpKqqltVfWEm2r/rxH9hXxY=
github.com/Rosettea/readline-1 v0.1.0-beta.0.20220228022904-61f5e4493011/go.mod h1:QiUAvbhg8PzCA4hlafCUl0bKD/0VmcocM4AjqtszAJs=
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20211022004519-f67a49cb50f5 h1:ygwVRX8gf5MHA0VzSgOdscCEoAJLjM8joEotfQPgAd0=
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20211022004519-f67a49cb50f5/go.mod h1:R09vh/04ILvP2Gj8/Z9Jd0Dh0ZIvaucowMEs6abQpWs=
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/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/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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
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/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=
@ -35,34 +28,36 @@ 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/layeh/gopher-luar v1.0.10 h1:8NIv4MX1Arz96kK4buGK1D87DyDxKZyq6KKvJ2diHp0=
github.com/layeh/gopher-luar v1.0.10/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk=
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/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/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=
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw=
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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/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-20220224120231-95c6836cb0e7 h1:BXxu8t6QN0G1uff4bzZzSkpsax8+ALqTGUtz08QrV00=
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/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/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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/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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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=

View File

@ -4,13 +4,14 @@ import (
"fmt"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib"
"github.com/chuckpreslar/emission"
"github.com/yuin/gopher-lua"
"layeh.com/gopher-luar"
)
type Bait struct{
Em *emission.Emitter
Loader packagelib.Loader
}
func New() Bait {
@ -19,15 +20,27 @@ func New() Bait {
emitter.Off(hookname, hookfunc)
fmt.Println(err)
})
return Bait{
b := Bait{
Em: emitter,
}
b.Loader = packagelib.Loader{
Load: b.loaderFunc,
Name: "bait",
}
return b
}
func (b *Bait) Loader(L *lua.LState) int {
mod := L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{})
func (b *Bait) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
exports := map[string]util.LuaExport{
"catch": util.LuaExport{b.bcatch, 2, false},
"catchOnce": util.LuaExport{b.bcatchOnce, 2, false},
"throw": util.LuaExport{b.bthrow, 1, true},
}
mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
util.Document(L, mod,
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
@ -36,35 +49,81 @@ 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.`)
L.SetField(mod, "throw", luar.New(L, b.bthrow))
L.SetField(mod, "catch", luar.New(L, b.bcatch))
L.SetField(mod, "catchOnce", luar.New(L, b.bcatchOnce))
return rt.TableValue(mod), nil
}
L.Push(mod)
return 1
func handleHook(t *rt.Thread, c *rt.GoCont, name string, catcher *rt.Closure, args ...interface{}) {
funcVal := rt.FunctionValue(catcher)
var luaArgs []rt.Value
for _, arg := range args {
var luarg rt.Value
switch arg.(type) {
case rt.Value: luarg = arg.(rt.Value)
default: luarg = rt.AsValue(arg)
}
luaArgs = append(luaArgs, luarg)
}
_, err := rt.Call1(t, funcVal, luaArgs...)
if err != nil {
e := rt.NewError(rt.StringValue(err.Error()))
e = e.AddContext(c.Next(), 1)
// panicking here won't actually cause hilbish to panic and instead will
// print the error and remove the hook (look at emission recover from above)
panic(e)
}
}
// throw(name, ...args)
// Throws a hook with `name` with the provided `args`
// --- @param name string
// --- @vararg any
func (b *Bait) bthrow(name string, args ...interface{}) {
b.Em.Emit(name, args...)
func (b *Bait) bthrow(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
name, err := c.StringArg(0)
if err != nil {
return nil, err
}
ifaceSlice := make([]interface{}, len(c.Etc()))
for i, v := range c.Etc() {
ifaceSlice[i] = v
}
b.Em.Emit(name, ifaceSlice...)
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(name string, catcher func(...interface{})) {
b.Em.On(name, catcher)
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.Em.On(name, func(args ...interface{}) {
handleHook(t, c, name, catcher, args...)
})
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(name string, catcher func(...interface{})) {
b.Em.Once(name, catcher)
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.Em.Once(name, func(args ...interface{}) {
handleHook(t, c, name, catcher, args...)
})
return c.Next(), nil
}

View File

@ -3,52 +3,68 @@ package commander
import (
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib"
"github.com/chuckpreslar/emission"
"github.com/yuin/gopher-lua"
)
type Commander struct{
Events *emission.Emitter
Loader packagelib.Loader
}
func New() Commander {
return Commander{
c := Commander{
Events: emission.NewEmitter(),
}
c.Loader = packagelib.Loader{
Load: c.loaderFunc,
Name: "commander",
}
return c
}
func (c *Commander) Loader(L *lua.LState) int {
exports := map[string]lua.LGFunction{
"register": c.cregister,
"deregister": c.cderegister,
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},
}
mod := L.SetFuncs(L.NewTable(), exports)
util.Document(L, mod, "Commander is Hilbish's custom command library, a way to write commands in Lua.")
L.Push(mod)
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 1
return rt.TableValue(mod), nil
}
// register(name, cb)
// Register a command with `name` that runs `cb` when ran
// --- @param name string
// --- @param cb function
func (c *Commander) cregister(L *lua.LState) int {
cmdName := L.CheckString(1)
cmd := L.CheckFunction(2)
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)
return 0
return ct.Next(), err
}
// deregister(name)
// Deregisters any command registered with `name`
// --- @param name string
func (c *Commander) cderegister(L *lua.LState) int {
cmdName := L.CheckString(1)
func (c *Commander) cderegister(t *rt.Thread, ct *rt.GoCont) (rt.Cont, error) {
if err := ct.Check1Arg(); err != nil {
return nil, err
}
cmdName, err := ct.StringArg(0)
if err != nil {
return nil, err
}
c.Events.Emit("commandDeregister", cmdName)
return 0
return ct.Next(), err
}

View File

@ -1,5 +1,3 @@
// 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 fs functions.
package fs
import (
@ -8,51 +6,70 @@ import (
"strings"
"hilbish/util"
"github.com/yuin/gopher-lua"
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib"
)
func Loader(L *lua.LState) int {
mod := L.SetFuncs(L.NewTable(), exports)
var Loader = packagelib.Loader{
Load: loaderFunc,
Name: "fs",
}
util.Document(L, mod, `The fs module provides easy and simple access to
func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
exports := map[string]util.LuaExport{
"cd": util.LuaExport{fcd, 1, false},
"mkdir": util.LuaExport{fmkdir, 2, false},
"stat": util.LuaExport{fstat, 1, false},
"readdir": util.LuaExport{freaddir, 1, false},
}
mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
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 fs functions.`)
L.Push(mod)
return 1
}
var exports = map[string]lua.LGFunction{
"cd": fcd,
"mkdir": fmkdir,
"stat": fstat,
"readdir": freaddir,
return rt.TableValue(mod), nil
}
// cd(dir)
// Changes directory to `dir`
// --- @param dir string
func fcd(L *lua.LState) int {
path := L.CheckString(1)
err := os.Chdir(strings.TrimSpace(path))
func fcd(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 {
e := err.(*os.PathError).Err.Error()
L.RaiseError(e + ": " + path)
return nil, err
}
return 0
err = os.Chdir(strings.TrimSpace(path))
if err != nil {
return nil, err
}
return c.Next(), err
}
// mkdir(name, recursive)
// Makes a directory called `name`. If `recursive` is true, it will create its parent directories.
// --- @param name string
// --- @param recursive boolean
func fmkdir(L *lua.LState) int {
dirname := L.CheckString(1)
recursive := L.ToBool(2)
func fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(2); err != nil {
return nil, err
}
dirname, err := c.StringArg(0)
if err != nil {
return nil, err
}
recursive, err := c.BoolArg(1)
if err != nil {
return nil, err
}
path := strings.TrimSpace(dirname)
var err error
if recursive {
err = os.MkdirAll(path, 0744)
@ -60,51 +77,58 @@ func fmkdir(L *lua.LState) int {
err = os.Mkdir(path, 0744)
}
if err != nil {
L.RaiseError(err.Error() + ": " + path)
return nil, err
}
return 0
return c.Next(), err
}
// stat(path)
// Returns info about `path`
// --- @param path string
func fstat(L *lua.LState) int {
path := L.CheckString(1)
func fstat(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
}
pathinfo, err := os.Stat(path)
if err != nil {
L.RaiseError(err.Error() + ": " + path)
return 0
return nil, err
}
statTbl := L.NewTable()
L.SetField(statTbl, "name", lua.LString(pathinfo.Name()))
L.SetField(statTbl, "size", lua.LNumber(pathinfo.Size()))
L.SetField(statTbl, "mode", lua.LString("0" + strconv.FormatInt(int64(pathinfo.Mode().Perm()), 8)))
L.SetField(statTbl, "isDir", lua.LBool(pathinfo.IsDir()))
L.Push(statTbl)
return 1
statTbl := rt.NewTable()
statTbl.Set(rt.StringValue("name"), rt.StringValue(pathinfo.Name()))
statTbl.Set(rt.StringValue("size"), rt.IntValue(pathinfo.Size()))
statTbl.Set(rt.StringValue("mode"), rt.StringValue("0" + strconv.FormatInt(int64(pathinfo.Mode().Perm()), 8)))
statTbl.Set(rt.StringValue("isDir"), rt.BoolValue(pathinfo.IsDir()))
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(L *lua.LState) int {
dir := L.CheckString(1)
names := L.NewTable()
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
}
names := rt.NewTable()
dirEntries, err := os.ReadDir(dir)
if err != nil {
L.RaiseError(err.Error() + ": " + dir)
return 0
return nil, err
}
for _, entry := range dirEntries {
names.Append(lua.LString(entry.Name()))
for i, entry := range dirEntries {
names.Set(rt.IntValue(int64(i + 1)), rt.StringValue(entry.Name()))
}
L.Push(names)
return 1
return c.PushingNext1(t.Runtime, rt.TableValue(names)), nil
}

View File

@ -5,76 +5,78 @@ import (
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib"
"golang.org/x/term"
"github.com/yuin/gopher-lua"
)
var termState *term.State
func Loader(L *lua.LState) int {
mod := L.SetFuncs(L.NewTable(), exports)
util.Document(L, mod, "The terminal library is a simple and lower level library for certain terminal interactions.")
L.Push(mod)
return 1
var Loader = packagelib.Loader{
Load: loaderFunc,
Name: "terminal",
}
var exports = map[string]lua.LGFunction{
"setRaw": termraw,
"restoreState": termrestoreState,
"size": termsize,
"saveState": termsaveState,
func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
exports := map[string]util.LuaExport{
"setRaw": util.LuaExport{termsetRaw, 0, false},
"restoreState": util.LuaExport{termrestoreState, 0, false},
"size": util.LuaExport{termsize, 0, false},
"saveState": util.LuaExport{termsaveState, 0, false},
}
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
func termsize(L *lua.LState) int {
func termsize(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
w, h, err := term.GetSize(int(os.Stdin.Fd()))
if err != nil {
L.RaiseError(err.Error())
return 0
return nil, err
}
dimensions := L.NewTable()
L.SetField(dimensions, "width", lua.LNumber(w))
L.SetField(dimensions, "height", lua.LNumber(h))
L.Push(dimensions)
return 1
dimensions := rt.NewTable()
dimensions.Set(rt.StringValue("width"), rt.IntValue(int64(w)))
dimensions.Set(rt.StringValue("height"), rt.IntValue(int64(h)))
return c.PushingNext1(t.Runtime, rt.TableValue(dimensions)), nil
}
// saveState()
// Saves the current state of the terminal
func termsaveState(L *lua.LState) int {
func termsaveState(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
state, err := term.GetState(int(os.Stdin.Fd()))
if err != nil {
L.RaiseError(err.Error())
return 0
return nil, err
}
termState = state
return 0
return c.Next(), nil
}
// restoreState()
// Restores the last saved state of the terminal
func termrestoreState(L *lua.LState) int {
func termrestoreState(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
err := term.Restore(int(os.Stdin.Fd()), termState)
if err != nil {
L.RaiseError(err.Error())
return nil, err
}
return 0
return c.Next(), nil
}
// setRaw()
// Puts the terminal in raw mode
func termraw(L *lua.LState) int {
func termsetRaw(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
_, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
L.RaiseError(err.Error())
return nil, err
}
return 0
return c.Next(), nil
}

View File

@ -78,3 +78,9 @@ func (h *fileHistory) Len() int {
func (h *fileHistory) Dump() interface{} {
return h.items
}
func (h *fileHistory) clear() {
h.items = []string{}
h.f.Truncate(0)
h.f.Sync()
}

142
job.go 100644
View File

@ -0,0 +1,142 @@
package main
import (
"sync"
"os"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
)
var jobs *jobHandler
type job struct {
cmd string
running bool
id int
pid int
exitCode int
proc *os.Process
}
func (j *job) start(pid int) {
j.pid = pid
j.running = true
hooks.Em.Emit("job.start", j.lua())
}
func (j *job) stop() {
// finish will be called in exec handle
j.proc.Kill()
}
func (j *job) finish() {
j.running = false
hooks.Em.Emit("job.done", j.lua())
}
func (j *job) setHandle(handle *os.Process) {
j.proc = handle
}
func (j *job) lua() rt.Value {
jobFuncs := map[string]util.LuaExport{
"stop": {j.luaStop, 0, false},
}
luaJob := rt.NewTable()
util.SetExports(l, luaJob, jobFuncs)
luaJob.Set(rt.StringValue("cmd"), rt.StringValue(j.cmd))
luaJob.Set(rt.StringValue("running"), rt.BoolValue(j.running))
luaJob.Set(rt.StringValue("id"), rt.IntValue(int64(j.id)))
luaJob.Set(rt.StringValue("pid"), rt.IntValue(int64(j.pid)))
luaJob.Set(rt.StringValue("exitCode"), rt.IntValue(int64(j.exitCode)))
return rt.TableValue(luaJob)
}
func (j *job) luaStop(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if j.running {
j.stop()
}
return c.Next(), nil
}
type jobHandler struct {
jobs map[int]*job
latestID int
mu *sync.RWMutex
}
func newJobHandler() *jobHandler {
return &jobHandler{
jobs: make(map[int]*job),
latestID: 0,
mu: &sync.RWMutex{},
}
}
func (j *jobHandler) add(cmd string) {
j.mu.Lock()
defer j.mu.Unlock()
j.latestID++
j.jobs[j.latestID] = &job{
cmd: cmd,
running: false,
id: j.latestID,
}
}
func (j *jobHandler) getLatest() *job {
j.mu.RLock()
defer j.mu.RUnlock()
return j.jobs[j.latestID]
}
func (j *jobHandler) loader(rtm *rt.Runtime) *rt.Table {
jobFuncs := map[string]util.LuaExport{
"all": {j.luaAllJobs, 0, false},
"get": {j.luaGetJob, 1, false},
}
luaJob := rt.NewTable()
util.SetExports(rtm, luaJob, jobFuncs)
return luaJob
}
func (j *jobHandler) luaGetJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
j.mu.RLock()
defer j.mu.RUnlock()
if err := c.Check1Arg(); err != nil {
return nil, err
}
jobID, err := c.IntArg(0)
if err != nil {
return nil, err
}
job := j.jobs[int(jobID)]
if job == nil {
return c.Next(), nil
}
return c.PushingNext1(t.Runtime, job.lua()), nil
}
func (j *jobHandler) luaAllJobs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
j.mu.RLock()
defer j.mu.RUnlock()
jobTbl := rt.NewTable()
for id, job := range j.jobs {
jobTbl.Set(rt.IntValue(int64(id)), job.lua())
}
return c.PushingNext1(t.Runtime, rt.TableValue(jobTbl)), nil
}

View File

@ -89,21 +89,25 @@ end
ansikit.print = function(text)
io.write(ansikit.format(text))
io.flush()
return ansikit
end
ansikit.printCode = function(code, terminate)
io.write(ansikit.getCode(code, terminate))
io.flush()
return ansikit
end
ansikit.printCSI = function(code, endc)
io.write(ansikit.getCSI(code, endc))
io.flush()
return ansikit
end
ansikit.println = function(text)
print(ansikit.print(text))
io.write(ansikit.format(text) .. "\n")
io.flush()
return ansikit
end

@ -1 +1 @@
Subproject commit 5a59d0f4543eb982593750c52f7393e2fd2d15f9
Subproject commit b362397a83e4516415c809c7d690b52e79a95f6e

47
lua.go
View File

@ -4,40 +4,42 @@ import (
"fmt"
"os"
"hilbish/util"
"hilbish/golibs/bait"
"hilbish/golibs/commander"
"hilbish/golibs/fs"
"hilbish/golibs/terminal"
"github.com/yuin/gopher-lua"
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib"
)
var minimalconf = `hilbish.prompt '& '`
func luaInit() {
l = lua.NewState()
l.OpenLibs()
l = rt.New(os.Stdout)
lib.LoadAll(l)
lib.LoadLibs(l, hilbishLoader)
// yes this is stupid, i know
l.PreloadModule("hilbish", hilbishLoader)
l.DoString("hilbish = require 'hilbish'")
util.DoString(l, "hilbish = require 'hilbish'")
// Add fs and terminal module module to Lua
l.PreloadModule("fs", fs.Loader)
l.PreloadModule("terminal", terminal.Loader)
lib.LoadLibs(l, fs.Loader)
lib.LoadLibs(l, terminal.Loader)
cmds := commander.New()
// When a command from Lua is added, register it for use
cmds.Events.On("commandRegister", func(cmdName string, cmd *lua.LFunction) {
cmds.Events.On("commandRegister", func(cmdName string, cmd *rt.Closure) {
commands[cmdName] = cmd
})
cmds.Events.On("commandDeregister", func(cmdName string) {
delete(commands, cmdName)
})
l.PreloadModule("commander", cmds.Loader)
lib.LoadLibs(l, cmds.Loader)
hooks = bait.New()
l.PreloadModule("bait", hooks.Loader)
lib.LoadLibs(l, hooks.Loader)
// Add Ctrl-C handler
hooks.Em.On("signal.sigint", func() {
@ -46,29 +48,28 @@ func luaInit() {
}
})
l.SetGlobal("complete", l.NewFunction(hlcomplete))
// Add more paths that Lua can require from
l.DoString("package.path = package.path .. " + requirePaths)
err := l.DoFile("prelude/init.lua")
err := util.DoString(l, "package.path = package.path .. " + requirePaths)
if err != nil {
err = l.DoFile(preloadPath)
fmt.Fprintln(os.Stderr, "Could not add preload paths! Libraries will be missing. This shouldn't happen.")
}
err = util.DoFile(l, "prelude/init.lua")
if err != nil {
err = util.DoFile(l, preloadPath)
if err != nil {
fmt.Fprintln(os.Stderr,
"Missing preload file, builtins may be missing.")
fmt.Fprintln(os.Stderr, "Missing preload file, builtins may be missing.")
}
}
}
func runConfig(confpath string) {
if !interactive {
return
}
err := l.DoFile(confpath)
err := util.DoFile(l, confpath)
if err != nil {
fmt.Fprintln(os.Stderr, err,
"\nAn error has occured while loading your config! Falling back to minimal default config.")
l.DoString(minimalconf)
fmt.Fprintln(os.Stderr, err, "\nAn error has occured while loading your config! Falling back to minimal default config.")
util.DoString(l, minimalconf)
}
}

62
main.go
View File

@ -10,20 +10,21 @@ import (
"runtime"
"strings"
"hilbish/util"
"hilbish/golibs/bait"
rt "github.com/arnodel/golua/runtime"
"github.com/pborman/getopt"
"github.com/yuin/gopher-lua"
"github.com/maxlandon/readline"
"golang.org/x/term"
)
var (
l *lua.LState
l *rt.Runtime
lr *lineReader
commands = map[string]*lua.LFunction{}
luaCompletions = map[string]*lua.LFunction{}
commands = map[string]*rt.Closure{}
luaCompletions = map[string]*rt.Closure{}
confDir string
userDataDir string
@ -43,7 +44,7 @@ func main() {
// i honestly dont know what directories to use for this
switch runtime.GOOS {
case "linux":
case "linux", "darwin":
userDataDir = getenv("XDG_DATA_HOME", curuser.HomeDir + "/.local/share")
default:
// this is fine on windows, dont know about others
@ -55,7 +56,7 @@ func main() {
defaultConfDir = filepath.Join(confDir, "hilbish")
} else {
// else do ~ substitution
defaultConfDir = expandHome(defaultHistDir)
defaultConfDir = filepath.Join(expandHome(defaultConfDir), "hilbish")
}
defaultConfPath = filepath.Join(defaultConfDir, "init.lua")
if defaultHistDir == "" {
@ -142,27 +143,28 @@ func main() {
scanner := bufio.NewScanner(bufio.NewReader(os.Stdin))
for scanner.Scan() {
text := scanner.Text()
runInput(text, text)
runInput(text, true)
}
exit(0)
}
if *cmdflag != "" {
runInput(*cmdflag, *cmdflag)
runInput(*cmdflag, true)
}
if getopt.NArgs() > 0 {
luaArgs := l.NewTable()
for _, arg := range getopt.Args() {
luaArgs.Append(lua.LString(arg))
luaArgs := rt.NewTable()
for i, arg := range getopt.Args() {
luaArgs.Set(rt.IntValue(int64(i)), rt.StringValue(arg))
}
l.SetGlobal("args", luaArgs)
err := l.DoFile(getopt.Arg(0))
l.GlobalEnv().Set(rt.StringValue("args"), rt.TableValue(luaArgs))
err := util.DoFile(l, getopt.Arg(0))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
exit(1)
}
os.Exit(0)
exit(0)
}
initialized = true
@ -185,7 +187,10 @@ input:
fmt.Println("^C")
continue
}
oldInput := input
var priv bool
if strings.HasPrefix(input, " ") {
priv = true
}
input = strings.TrimSpace(input)
if len(input) == 0 {
@ -198,6 +203,8 @@ input:
for {
input, err = continuePrompt(input)
if err != nil {
running = true
lr.SetPrompt(fmtPrompt(prompt))
goto input // continue inside nested loop
}
if !strings.HasSuffix(input, "\\") {
@ -206,7 +213,7 @@ input:
}
}
runInput(input, oldInput)
runInput(input, priv)
termwidth, _, err := term.GetSize(0)
if err != nil {
@ -268,8 +275,7 @@ func handleHistory(cmd string) {
func expandHome(path string) string {
homedir := curuser.HomeDir
return strings.Replace(defaultHistDir, "~", homedir, 1)
return strings.Replace(path, "~", homedir, 1)
}
func removeDupes(slice []string) []string {
@ -284,3 +290,21 @@ func removeDupes(slice []string) []string {
return newSlice
}
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
func exit(code int) {
// wait for all timers to finish before exiting
for {
if timers.running == 0 {
os.Exit(code)
}
}
}

View File

@ -8,7 +8,7 @@ local _ = require 'succulent' -- Function additions
local oldDir = hilbish.cwd()
local shlvl = tonumber(os.getenv 'SHLVL')
if shlvl ~= nil then os.setenv('SHLVL', shlvl + 1) else os.setenv('SHLVL', 0) end
if shlvl ~= nil then os.setenv('SHLVL', tostring(shlvl + 1)) else os.setenv('SHLVL', '0') end
-- Builtins
local recentDirs = {}
@ -168,6 +168,9 @@ hilbish.userDir.config .. '/hilbish/init.lua' ..
and also change all global functions (prompt, alias) to be
in the hilbish module (hilbish.prompt, hilbish.alias as examples).
And if this is your first time (most likely), you can copy a config
from ]] .. hilbish.dataDir,
[[
Since 1.0 is a big release, you'll want to check the changelog
at https://github.com/Rosettea/Hilbish/releases/tag/v1.0.0
to find more breaking changes.
@ -214,14 +217,6 @@ do
end
end,
})
bait.catch('command.exit', function ()
for key, value in pairs(virt_G) do
if type(value) == 'string' then
virt_G[key] = os.getenv(key)
end
end
end)
end
commander.register('cdr', function(args)
@ -263,7 +258,7 @@ bait.catch('command.not-found', function(cmd)
print(string.format('hilbish: %s not found', cmd))
end)
bait.catch('command.no-perm', function(cmd)
print(string.format('hilbish: %s: no permission', cmd))
bait.catch('command.not-executable', function(cmd)
print(string.format('hilbish: %s: not executable', cmd))
end)

View File

@ -34,32 +34,39 @@ const (
charCtrlHat // ^^
charCtrlUnderscore // ^_
charBackspace2 = 127 // ASCII 1963
)
// Escape sequences
var (
seqUp = string([]byte{27, 91, 65})
seqDown = string([]byte{27, 91, 66})
seqForwards = string([]byte{27, 91, 67})
seqBackwards = string([]byte{27, 91, 68})
seqHome = string([]byte{27, 91, 72})
seqHomeSc = string([]byte{27, 91, 49, 126})
seqEnd = string([]byte{27, 91, 70})
seqEndSc = string([]byte{27, 91, 52, 126})
seqDelete = string([]byte{27, 91, 51, 126})
seqShiftTab = string([]byte{27, 91, 90})
seqAltQuote = string([]byte{27, 34}) // Added for showing registers ^["
seqAltR = string([]byte{27, 114}) // Used for alternative history
seqUp = string([]byte{27, 91, 65})
seqDown = string([]byte{27, 91, 66})
seqForwards = string([]byte{27, 91, 67})
seqBackwards = string([]byte{27, 91, 68})
seqHome = string([]byte{27, 91, 72})
seqHomeSc = string([]byte{27, 91, 49, 126})
seqEnd = string([]byte{27, 91, 70})
seqEndSc = string([]byte{27, 91, 52, 126})
seqDelete = string([]byte{27, 91, 51, 126})
seqDelete2 = string([]byte{27, 91, 80})
seqCtrlDelete = string([]byte{27, 91, 51, 59, 53, 126})
seqCtrlDelete2 = string([]byte{27, 91, 77})
seqAltDelete = string([]byte{27, 91, 51, 59, 51, 126})
seqShiftTab = string([]byte{27, 91, 90})
seqAltQuote = string([]byte{27, 34}) // Added for showing registers ^["
seqAltB = string([]byte{27, 98})
seqAltD = string([]byte{27, 100})
seqAltF = string([]byte{27, 102})
seqAltR = string([]byte{27, 114}) // Used for alternative history
seqAltBackspace = string([]byte{27, 127})
)
const (
seqPosSave = "\x1b[s"
seqPosRestore = "\x1b[u"
seqClearLineAfer = "\x1b[0k"
seqClearLineBefore = "\x1b[1k"
seqClearLine = "\x1b[2k"
seqClearLineAfer = "\x1b[0K"
seqClearLineBefore = "\x1b[1K"
seqClearLine = "\x1b[2K"
seqClearScreenBelow = "\x1b[0J"
seqClearScreen = "\x1b[2J" // Clears screen fully
seqCursorTopLeft = "\x1b[H" // Clears screen and places cursor on top-left
@ -78,6 +85,7 @@ const (
seqBold = "\x1b[1m"
seqUnderscore = "\x1b[4m"
seqBlink = "\x1b[5m"
seqInvert = "\x1b[7m"
)
// Text colours

View File

@ -121,7 +121,7 @@ func (g *CompletionGroup) writeGrid(rl *Instance) (comp string) {
}
if (x == g.tcPosX && y == g.tcPosY) && (g.isCurrent) {
comp += seqCtermFg255 + seqFgBlackBright
comp += seqInvert
}
comp += fmt.Sprintf("%-"+cellWidth+"s %s", g.Suggestions[i], seqReset)

View File

@ -188,7 +188,7 @@ func (g *CompletionGroup) writeList(rl *Instance) (comp string) {
// function highlights the cell depending on current selector place.
highlight := func(y int, x int) string {
if y == g.tcPosY && x == g.tcPosX && g.isCurrent {
return seqCtermFg255 + seqFgBlackBright
return seqInvert
}
return ""
}

View File

@ -101,7 +101,7 @@ func (g *CompletionGroup) writeMap(rl *Instance) (comp string) {
// Highlighting function
highlight := func(y int) string {
if y == g.tcPosY && g.isCurrent {
return seqCtermFg255 + seqFgBlackBright
return seqInvert
}
return ""
}

View File

@ -100,12 +100,12 @@ func moveCursorBackwards(i int) {
printf("\x1b[%dD", i)
}
func (rl *Instance) backspace() {
func (rl *Instance) backspace(forward bool) {
if len(rl.line) == 0 || rl.pos == 0 {
return
}
rl.deleteBackspace()
rl.deleteBackspace(forward)
}
func (rl *Instance) moveCursorByAdjust(adjust int) {

View File

@ -7,7 +7,7 @@ type EventReturn struct {
ForwardKey bool
ClearHelpers bool
CloseReadline bool
HintText []rune
InfoText []rune
NewLine []rune
NewPos int
}

View File

@ -4,10 +4,12 @@ import "regexp"
// SetHintText - a nasty function to force writing a new hint text. It does not update helpers, it just renders
// them, so the hint will survive until the helpers (thus including the hint) will be updated/recomputed.
/*
func (rl *Instance) SetHintText(s string) {
rl.hintText = []rune(s)
rl.renderHelpers()
}
*/
func (rl *Instance) getHintText() {
@ -27,7 +29,7 @@ func (rl *Instance) getHintText() {
// writeHintText - only writes the hint text and computes its offsets.
func (rl *Instance) writeHintText() {
if len(rl.hintText) == 0 {
rl.hintY = 0
//rl.hintY = 0
return
}
@ -41,16 +43,16 @@ func (rl *Instance) writeHintText() {
wrapped, hintLen := WrapText(string(rl.hintText), width)
offset += hintLen
rl.hintY = offset
// rl.hintY = offset
hintText := string(wrapped)
if len(hintText) > 0 {
print("\r" + rl.HintFormatting + string(hintText) + seqReset)
print(rl.HintFormatting + string(hintText) + seqReset)
}
}
func (rl *Instance) resetHintText() {
rl.hintY = 0
//rl.hintY = 0
rl.hintText = []rune{}
}

View File

@ -183,13 +183,13 @@ func (rl *Instance) completeHistory() (hist []*CompletionGroup) {
return
}
history = rl.altHistory
rl.histHint = []rune(rl.altHistName + ": ")
rl.histInfo = []rune(rl.altHistName + ": ")
} else {
if rl.mainHistory == nil {
return
}
history = rl.mainHistory
rl.histHint = []rune(rl.mainHistName + ": ")
rl.histInfo = []rune(rl.mainHistName + ": ")
}
hist[0].init(rl)

56
readline/info.go 100644
View File

@ -0,0 +1,56 @@
package readline
import "regexp"
// SetInfoText - a nasty function to force writing a new info text. It does not update helpers, it just renders
// them, so the info will survive until the helpers (thus including the info) will be updated/recomputed.
func (rl *Instance) SetInfoText(s string) {
rl.infoText = []rune(s)
rl.renderHelpers()
}
func (rl *Instance) getInfoText() {
if !rl.modeAutoFind && !rl.modeTabFind {
// Return if no infos provided by the user/engine
if rl.InfoText == nil {
rl.resetInfoText()
return
}
// The info text also works with the virtual completion line system.
// This way, the info is also refreshed depending on what we are pointing
// at with our cursor.
rl.infoText = rl.InfoText(rl.getCompletionLine())
}
}
// writeInfoText - only writes the info text and computes its offsets.
func (rl *Instance) writeInfoText() {
if len(rl.infoText) == 0 {
rl.infoY = 0
return
}
width := GetTermWidth()
// Wraps the line, and counts the number of newlines in the string,
// adjusting the offset as well.
re := regexp.MustCompile(`\r?\n`)
newlines := re.Split(string(rl.infoText), -1)
offset := len(newlines)
wrapped, infoLen := WrapText(string(rl.infoText), width)
offset += infoLen
rl.infoY = offset
infoText := string(wrapped)
if len(infoText) > 0 {
print("\r" + rl.InfoFormatting + string(infoText) + seqReset)
}
}
func (rl *Instance) resetInfoText() {
rl.infoY = 0
rl.infoText = []rune{}
}

View File

@ -30,11 +30,13 @@ type Instance struct {
Multiline bool // If set to true, the shell will have a two-line prompt.
MultilinePrompt string // If multiline is true, this is the content of the 2nd line.
mainPrompt string // If multiline true, the full prompt string / If false, the 1st line of the prompt
realPrompt []rune // The prompt that is actually on the same line as the beginning of the input line.
defaultPrompt []rune
promptLen int
stillOnRefresh bool // True if some logs have printed asynchronously since last loop. Check refresh prompt funcs
mainPrompt string // If multiline true, the full prompt string / If false, the 1st line of the prompt
rightPrompt string
rightPromptLen int
realPrompt []rune // The prompt that is actually on the same line as the beginning of the input line.
defaultPrompt []rune
promptLen int
stillOnRefresh bool // True if some logs have printed asynchronously since last loop. Check refresh prompt funcs
//
// Input Line ---------------------------------------------------------------------------------
@ -110,7 +112,7 @@ type Instance struct {
searchMode FindMode // Used for varying hints, and underlying functions called
regexSearch *regexp.Regexp // Holds the current search regex match
mainHist bool // Which history stdin do we want
histHint []rune // We store a hist hint, for dual history sources
histInfo []rune // We store a piece of hist info, for dual history sources
//
// History -----------------------------------------------------------------------------------
@ -134,19 +136,33 @@ type Instance struct {
histNavIdx int // Used for quick history navigation.
//
// Hints -------------------------------------------------------------------------------------
// Info -------------------------------------------------------------------------------------
// HintText is a helper function which displays hint text the prompt.
// HintText takes the line input from the promt and the cursor position.
// InfoText is a helper function which displays infio text below the prompt.
// InfoText takes the line input from the prompt and the cursor position.
// It returns the info text to display.
InfoText func([]rune, int) []rune
// InfoColor is any ANSI escape codes you wish to use for info formatting. By
// default this will just be blue.
InfoFormatting string
infoText []rune // The actual info text
infoY int // Offset to info, if it spans multiple lines
//
// Hints -----------------------------------------------------------------------------------
// HintText is a helper function which displays hint text right after the user's input.
// It takes the line input and cursor position.
// It returns the hint text to display.
HintText func([]rune, int) []rune
// HintColor any ANSI escape codes you wish to use for hint formatting. By
// default this will just be blue.
// HintFormatting is just a string to use as the formatting for the hint. By default
// this will be a grey color.
HintFormatting string
hintText []rune // The actual hint text
hintY int // Offset to hints, if it spans multiple lines
hintText []rune
//
// Vim Operatng Parameters -------------------------------------------------------------------
@ -205,7 +221,8 @@ func NewInstance() *Instance {
rl.HistoryAutoWrite = true
// Others
rl.HintFormatting = seqFgBlue
rl.InfoFormatting = seqFgBlue
rl.HintFormatting = "\x1b[2m"
rl.evtKeyPress = make(map[string]func(string, []rune, int) *EventReturn)
rl.TempDirectory = os.TempDir()

View File

@ -57,9 +57,9 @@ func (rl *Instance) echo() {
// Print the input line with optional syntax highlighting
if rl.SyntaxHighlighter != nil {
print(rl.SyntaxHighlighter(line) + " ")
print(rl.SyntaxHighlighter(line))
} else {
print(string(line) + " ")
print(string(line))
}
}
@ -125,14 +125,14 @@ func (rl *Instance) deleteX() {
rl.updateHelpers()
}
func (rl *Instance) deleteBackspace() {
func (rl *Instance) deleteBackspace(forward bool) {
switch {
case len(rl.line) == 0:
return
case rl.pos == 0:
rl.line = rl.line[1:]
case forward:
rl.line = append(rl.line[:rl.pos], rl.line[rl.pos+1:]...)
case rl.pos > len(rl.line):
rl.backspace() // There is an infite loop going on here...
rl.backspace(forward) // There is an infite loop going on here...
case rl.pos == len(rl.line):
rl.pos--
rl.line = rl.line[:rl.pos]
@ -176,3 +176,48 @@ func (rl *Instance) deleteToBeginning() {
rl.line = rl.line[rl.pos:]
rl.pos = 0
}
func (rl *Instance) deleteToEnd() {
rl.resetVirtualComp(false)
// Keep everything before the cursor
rl.line = rl.line[:rl.pos]
}
// @TODO(Renzix): move to emacs sepecific file
func (rl *Instance) emacsForwardWord(tokeniser tokeniser) (adjust int) {
split, index, pos := tokeniser(rl.line, rl.pos)
if len(split) == 0 {
return
}
word := strings.TrimSpace(split[index])
switch {
case len(split) == 0:
return
case pos == len(word) && index != len(split)-1:
extrawhitespace := len(strings.TrimLeft(split[index], " ")) - len(word)
word = split[index+1]
adjust = len(word) + extrawhitespace
default:
adjust = len(word) - pos
}
return
}
func (rl *Instance) emacsBackwardWord(tokeniser tokeniser) (adjust int) {
split, index, pos := tokeniser(rl.line, rl.pos)
if len(split) == 0 {
return
}
switch {
case len(split) == 0:
return
case pos == 0 && index != 0:
adjust = len(split[index-1])
default:
adjust = pos
}
return
}

View File

@ -11,6 +11,13 @@ import (
// It also calculates the runes in the string as well as any non-printable escape codes.
func (rl *Instance) SetPrompt(s string) {
rl.mainPrompt = s
rl.computePrompt()
}
// SetRightPrompt sets the right prompt.
func (rl *Instance) SetRightPrompt(s string) {
rl.rightPrompt = s + " "
rl.computePrompt()
}
// RefreshPromptLog - A simple function to print a string message (a log, or more broadly,
@ -20,7 +27,7 @@ func (rl *Instance) RefreshPromptLog(log string) (err error) {
// We adjust cursor movement, depending on which mode we're currently in.
if !rl.modeTabCompletion {
rl.tcUsedY = 1
// Account for the hint line
// Account for the info line
} else if rl.modeTabCompletion && rl.modeAutoFind {
rl.tcUsedY = 0
} else {
@ -40,7 +47,7 @@ func (rl *Instance) RefreshPromptLog(log string) (err error) {
moveCursorUp(1)
}
rl.stillOnRefresh = true
moveCursorUp(rl.hintY + rl.tcUsedY)
moveCursorUp(rl.infoY + rl.tcUsedY)
moveCursorBackwards(GetTermWidth())
print("\r\n" + seqClearScreenBelow)
@ -68,12 +75,11 @@ func (rl *Instance) RefreshPromptLog(log string) (err error) {
// RefreshPromptInPlace - Refreshes the prompt in the very same place he is.
func (rl *Instance) RefreshPromptInPlace(prompt string) (err error) {
// We adjust cursor movement, depending on which mode we're currently in.
// Prompt data intependent
if !rl.modeTabCompletion {
rl.tcUsedY = 1
// Account for the hint line
// Account for the info line
} else if rl.modeTabCompletion && rl.modeAutoFind {
rl.tcUsedY = 0
} else {
@ -82,7 +88,7 @@ func (rl *Instance) RefreshPromptInPlace(prompt string) (err error) {
// Update the prompt if a special has been passed.
if prompt != "" {
rl.mainPrompt = prompt
rl.SetPrompt(prompt)
}
if rl.Multiline {
@ -91,7 +97,7 @@ func (rl *Instance) RefreshPromptInPlace(prompt string) (err error) {
// Clear the input line and everything below
print(seqClearLine)
moveCursorUp(rl.hintY + rl.tcUsedY)
moveCursorUp(rl.infoY + rl.tcUsedY)
moveCursorBackwards(GetTermWidth())
print("\r\n" + seqClearScreenBelow)
@ -118,7 +124,7 @@ func (rl *Instance) RefreshPromptCustom(prompt string, offset int, clearLine boo
// We adjust cursor movement, depending on which mode we're currently in.
if !rl.modeTabCompletion {
rl.tcUsedY = 1
} else if rl.modeTabCompletion && rl.modeAutoFind { // Account for the hint line
} else if rl.modeTabCompletion && rl.modeAutoFind { // Account for the info line
rl.tcUsedY = 0
} else {
rl.tcUsedY = 1
@ -137,7 +143,7 @@ func (rl *Instance) RefreshPromptCustom(prompt string, offset int, clearLine boo
// Update the prompt if a special has been passed.
if prompt != "" {
rl.mainPrompt = prompt
rl.SetPrompt(prompt)
}
// Add a new line if needed
@ -185,6 +191,7 @@ func (rl *Instance) computePrompt() (prompt []rune) {
// Strip color escapes
rl.promptLen = getRealLength(string(rl.realPrompt))
rl.rightPromptLen = getRealLength(string(rl.rightPrompt))
return
}
@ -205,3 +212,11 @@ func getRealLength(s string) (l int) {
stripped := ansi.Strip(s)
return uniseg.GraphemeClusterCount(stripped)
}
func (rl *Instance) echoRightPrompt() {
if rl.fullX < GetTermWidth() - rl.rightPromptLen - 1 {
moveCursorForwards(GetTermWidth())
moveCursorBackwards(rl.rightPromptLen)
print(rl.rightPrompt)
}
}

View File

@ -2,9 +2,11 @@ package readline
import (
"bytes"
"errors"
"fmt"
"os"
"regexp"
"syscall"
)
var rxMultiline = regexp.MustCompile(`[\r\n]+`)
@ -38,11 +40,12 @@ func (rl *Instance) Readline() (string, error) {
rl.modeViMode = VimInsert
rl.pos = 0
rl.posY = 0
rl.tcPrefix = ""
// Completion && hints init
rl.resetHintText()
// Completion && infos init
rl.resetInfoText()
rl.resetTabCompletion()
rl.getHintText()
rl.getInfoText()
// History Init
// We need this set to the last command, so that we can access it quickly
@ -62,7 +65,7 @@ func (rl *Instance) Readline() (string, error) {
return string(rl.line), nil
}
// Finally, print any hints or completions
// Finally, print any info or completions
// if the TabCompletion engines so desires
rl.renderHelpers()
@ -76,6 +79,12 @@ func (rl *Instance) Readline() (string, error) {
var err error
i, err = os.Stdin.Read(b)
if err != nil {
if errors.Is(err, syscall.EAGAIN) {
err = syscall.SetNonblock(syscall.Stdin, false)
if err == nil {
continue
}
}
return "", err
}
}
@ -127,8 +136,8 @@ func (rl *Instance) Readline() (string, error) {
rl.updateHelpers()
}
if len(ret.HintText) > 0 {
rl.hintText = ret.HintText
if len(ret.InfoText) > 0 {
rl.infoText = ret.InfoText
rl.clearHelpers()
rl.renderHelpers()
}
@ -160,9 +169,18 @@ func (rl *Instance) Readline() (string, error) {
rl.clearHelpers()
return "", CtrlC
case charEOF:
rl.clearHelpers()
return "", EOF
case charEOF: // ctrl d
if len(rl.line) == 0 {
rl.clearHelpers()
return "", EOF
}
if rl.modeTabFind {
rl.backspaceTabFind()
} else {
if (rl.pos < len(rl.line)) {
rl.deleteBackspace(true)
}
}
// Clear screen
case charCtrlL:
@ -173,8 +191,8 @@ func (rl *Instance) Readline() (string, error) {
}
print(seqClearScreenBelow)
rl.resetHintText()
rl.getHintText()
rl.resetInfoText()
rl.getInfoText()
rl.renderHelpers()
// Line Editing ------------------------------------------------------------------------------------
@ -188,6 +206,16 @@ func (rl *Instance) Readline() (string, error) {
rl.resetHelpers()
rl.updateHelpers()
case charCtrlK:
if rl.modeTabCompletion {
rl.resetVirtualComp(true)
}
// Delete everything after the cursor position
rl.saveBufToRegister(rl.line[rl.pos:])
rl.deleteToEnd()
rl.resetHelpers()
rl.updateHelpers()
case charBackspace, charBackspace2:
// When currently in history completion, we refresh and automatically
// insert the first (filtered) candidate, virtually
@ -213,7 +241,7 @@ func (rl *Instance) Readline() (string, error) {
// Vim mode has different behaviors
if rl.InputMode == Vim {
if rl.modeViMode == VimInsert {
rl.backspace()
rl.backspace(false)
} else if rl.pos != 0 {
rl.pos--
}
@ -222,7 +250,7 @@ func (rl *Instance) Readline() (string, error) {
}
// Else emacs deletes a character
rl.backspace()
rl.backspace(false)
rl.renderHelpers()
}
@ -387,6 +415,10 @@ func (rl *Instance) Readline() (string, error) {
rl.renderHelpers()
}
case charCtrlUnderscore:
rl.undoLast()
rl.viUndoSkipAppend = true
case '\r':
fallthrough
case '\n':
@ -516,22 +548,27 @@ func (rl *Instance) editorInput(r []rune) {
case VimReplaceMany:
for _, char := range r {
rl.deleteX()
if rl.pos != len(rl.line) {
rl.deleteX()
}
rl.insert([]rune{char})
}
rl.refreshVimStatus()
default:
// For some reason Ctrl+k messes with the input line, so ignore it.
if r[0] == 11 {
// Don't insert control keys
if r[0] >= 1 && r[0] <= 31 {
return
}
// We reset the history nav counter each time we come here:
// We don't need it when inserting text.
rl.histNavIdx = 0
rl.insert(r)
rl.writeHintText()
}
rl.echoRightPrompt()
if len(rl.multisplit) == 0 {
rl.syntaxCompletion()
}
@ -625,6 +662,8 @@ func (rl *Instance) escapeSeq(r []rune) {
}
rl.mainHist = true
rl.walkHistory(1)
moveCursorForwards(len(rl.line) - rl.pos)
rl.pos = len(rl.line)
case seqDown:
if rl.modeTabCompletion {
@ -636,6 +675,8 @@ func (rl *Instance) escapeSeq(r []rune) {
}
rl.mainHist = true
rl.walkHistory(-1)
moveCursorForwards(len(rl.line) - rl.pos)
rl.pos = len(rl.line)
case seqForwards:
if rl.modeTabCompletion {
@ -647,8 +688,7 @@ func (rl *Instance) escapeSeq(r []rune) {
}
if (rl.modeViMode == VimInsert && rl.pos < len(rl.line)) ||
(rl.modeViMode != VimInsert && rl.pos < len(rl.line)-1) {
moveCursorForwards(1)
rl.pos++
rl.moveCursorByAdjust(1)
}
rl.updateHelpers()
rl.viUndoSkipAppend = true
@ -663,10 +703,7 @@ func (rl *Instance) escapeSeq(r []rune) {
rl.renderHelpers()
return
}
if rl.pos > 0 {
moveCursorBackwards(1)
rl.pos--
}
rl.moveCursorByAdjust(-1)
rl.viUndoSkipAppend = true
rl.updateHelpers()
@ -689,32 +726,64 @@ func (rl *Instance) escapeSeq(r []rune) {
rl.updateHelpers()
return
case seqCtrlRightArrow:
rl.insert(rl.hintText)
rl.moveCursorByAdjust(rl.viJumpW(tokeniseLine))
rl.updateHelpers()
return
case seqDelete:
case seqDelete,seqDelete2:
if rl.modeTabFind {
rl.backspaceTabFind()
} else {
rl.deleteBackspace()
if (rl.pos < len(rl.line)) {
rl.deleteBackspace(true)
}
}
case seqHome, seqHomeSc:
if rl.modeTabCompletion {
return
}
moveCursorBackwards(rl.pos)
rl.pos = 0
rl.moveCursorByAdjust(-rl.pos)
rl.updateHelpers()
rl.viUndoSkipAppend = true
case seqEnd, seqEndSc:
if rl.modeTabCompletion {
return
}
moveCursorForwards(len(rl.line) - rl.pos)
rl.pos = len(rl.line)
rl.moveCursorByAdjust(len(rl.line) - rl.pos)
rl.updateHelpers()
rl.viUndoSkipAppend = true
case seqAltB:
if rl.modeTabCompletion {
return
}
// This is only available in Insert mode
if rl.modeViMode != VimInsert {
return
}
move := rl.emacsBackwardWord(tokeniseLine)
rl.moveCursorByAdjust(-move)
rl.updateHelpers()
case seqAltF:
if rl.modeTabCompletion {
return
}
// This is only available in Insert mode
if rl.modeViMode != VimInsert {
return
}
move := rl.emacsForwardWord(tokeniseLine)
rl.moveCursorByAdjust(move)
rl.updateHelpers()
case seqAltR:
rl.resetVirtualComp(false)
// For some modes only, if we are in vim Keys mode,
@ -733,6 +802,36 @@ func (rl *Instance) escapeSeq(r []rune) {
rl.updateTabFind([]rune{})
rl.viUndoSkipAppend = true
case seqAltBackspace:
if rl.modeTabCompletion {
rl.resetVirtualComp(false)
}
// This is only available in Insert mode
if rl.modeViMode != VimInsert {
return
}
rl.saveToRegister(rl.viJumpB(tokeniseLine))
rl.viDeleteByAdjust(rl.viJumpB(tokeniseLine))
rl.updateHelpers()
case seqCtrlDelete, seqCtrlDelete2, seqAltD:
if rl.modeTabCompletion {
rl.resetVirtualComp(false)
}
rl.saveToRegister(rl.emacsForwardWord(tokeniseLine))
// vi delete, emacs forward, funny huh
rl.viDeleteByAdjust(rl.emacsForwardWord(tokeniseLine))
rl.updateHelpers()
case seqAltDelete:
if rl.modeTabCompletion {
rl.resetVirtualComp(false)
}
rl.saveToRegister(-rl.emacsBackwardWord(tokeniseLine))
rl.viDeleteByAdjust(-rl.emacsBackwardWord(tokeniseLine))
rl.updateHelpers()
default:
if rl.modeTabFind {
return
@ -768,6 +867,8 @@ func (rl *Instance) escapeSeq(r []rune) {
}
func (rl *Instance) carridgeReturn() {
rl.moveCursorByAdjust(len(rl.line))
rl.updateHelpers()
rl.clearHelpers()
print("\r\n")
if rl.HistoryAutoWrite {

View File

@ -259,9 +259,9 @@ func (r *registers) resetRegister() {
// The user can show registers completions and insert, no matter the cursor position.
func (rl *Instance) completeRegisters() (groups []*CompletionGroup) {
// We set the hint exceptionally
hint := BLUE + "-- registers --" + RESET
rl.hintText = []rune(hint)
// We set the info exceptionally
info := BLUE + "-- registers --" + RESET
rl.infoText = []rune(info)
// Make the groups
anonRegs := &CompletionGroup{

View File

@ -73,7 +73,12 @@ func (rl *Instance) insertCandidate() {
// Ensure no indexing error happens with prefix
if len(completion) >= prefix {
rl.insert([]rune(completion[prefix:]))
comp := completion[prefix:]
if completion[:prefix] != rl.tcPrefix {
rl.viDeleteByAdjust(-prefix)
comp = completion
}
rl.insert([]rune(comp))
if !cur.TrimSlash && !cur.NoSpace {
rl.insert([]rune(" "))
}

View File

@ -93,19 +93,16 @@ func (rl *Instance) getTabSearchCompletion() {
}
rl.getCurrentGroup()
// Set the hint for this completion mode
rl.hintText = append([]rune("Completion search: "), rl.tfLine...)
// Set the hint for this completion mode
rl.hintText = append([]rune("Completion search: "), rl.tfLine...)
// Set the info for this completion mode
rl.infoText = append([]rune("Completion search: "), rl.tfLine...)
for _, g := range rl.tcGroups {
g.updateTabFind(rl)
}
// If total number of matches is zero, we directly change the hint, and return
// If total number of matches is zero, we directly change the info, and return
if comps, _, _ := rl.getCompletionCount(); comps == 0 {
rl.hintText = append(rl.hintText, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...)
rl.infoText = append(rl.infoText, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...)
}
}
@ -120,25 +117,25 @@ func (rl *Instance) getHistorySearchCompletion() {
rl.tcGroups = checkNilItems(rl.tcGroups) // Avoid nil maps in groups
rl.getCurrentGroup() // Make sure there is a current group
// The history hint is already set, but overwrite it if we don't have completions
// The history info is already set, but overwrite it if we don't have completions
if len(rl.tcGroups[0].Suggestions) == 0 {
rl.histHint = []rune(fmt.Sprintf("%s%s%s %s", DIM, RED,
rl.histInfo = []rune(fmt.Sprintf("%s%s%s %s", DIM, RED,
"No command history source, or empty (Ctrl-G/Esc to cancel)", RESET))
rl.hintText = rl.histHint
rl.infoText = rl.histInfo
return
}
// Set the hint line with everything
rl.histHint = append([]rune("\033[38;5;183m"+string(rl.histHint)+RESET), rl.tfLine...)
rl.histHint = append(rl.histHint, []rune(RESET)...)
rl.hintText = rl.histHint
// Set the info line with everything
rl.histInfo = append([]rune("\033[38;5;183m"+string(rl.histInfo)+RESET), rl.tfLine...)
rl.histInfo = append(rl.histInfo, []rune(RESET)...)
rl.infoText = rl.histInfo
// Refresh filtered candidates
rl.tcGroups[0].updateTabFind(rl)
// If no items matched history, add hint text that we failed to search
// If no items matched history, add info text that we failed to search
if len(rl.tcGroups[0].Suggestions) == 0 {
rl.hintText = append(rl.histHint, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...)
rl.infoText = append(rl.histInfo, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...)
return
}
}
@ -301,15 +298,15 @@ func (rl *Instance) cropCompletions(comps string) (cropped string, usedY int) {
// Else we go on, but we have more comps than what allowed:
// we will add a line to the end of the comps, giving the actualized
// number of completions remaining and not printed
var moreComps = func(cropped string, offset int) (hinted string, noHint bool) {
var moreComps = func(cropped string, offset int) (infoed string, noInfo bool) {
_, _, adjusted := rl.getCompletionCount()
remain := adjusted - offset
if remain == 0 {
return cropped, true
}
hint := fmt.Sprintf(DIM+YELLOW+" %d more completions... (scroll down to show)"+RESET+"\n", remain)
hinted = cropped + hint
return hinted, false
info := fmt.Sprintf(DIM+YELLOW+" %d more completions... (scroll down to show)"+RESET+"\n", remain)
infoed = cropped + info
return infoed, false
}
// Get the current absolute candidate position (prev groups x suggestions + curGroup.tcPosY)
@ -512,7 +509,7 @@ func (rl *Instance) hasOneCandidate() bool {
// - The terminal lengh
// we use this function to prompt for confirmation before printing comps.
func (rl *Instance) promptCompletionConfirm(sentence string) {
rl.hintText = []rune(sentence)
rl.infoText = []rune(sentence)
rl.compConfirmWait = true
rl.viUndoSkipAppend = true

View File

@ -33,7 +33,7 @@ func (rl *Instance) updateTabFind(r []rune) {
var err error
rl.regexSearch, err = regexp.Compile("(?i)" + string(rl.tfLine))
if err != nil {
rl.hintText = []rune(Red("Failed to match search regexp"))
rl.infoText = []rune(Red("Failed to match search regexp"))
}
// We update and print

View File

@ -1,12 +1,15 @@
package readline
import "golang.org/x/text/width"
// updateHelpers is a key part of the whole refresh process:
// it should coordinate reprinting the input line, any hints and completions
// 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() {
// Load all hints & completions before anything.
// Thus overwrites anything having been dirtily added/forced/modified, like rl.SetHintText()
// Load all Infos & completions before anything.
// Thus overwrites anything having been dirtily added/forced/modified, like rl.SetInfoText()
rl.getInfoText()
rl.getHintText()
if rl.modeTabCompletion {
rl.getTabCompletion()
@ -20,6 +23,23 @@ func (rl *Instance) updateHelpers() {
rl.renderHelpers()
}
const tabWidth = 4
func getWidth(x []rune) int {
var w int
for _, j := range x {
k := width.LookupRune(j).Kind()
if j == '\t' {
w += tabWidth
} else if k == width.EastAsianWide || k == width.EastAsianFullwidth {
w += 2
} else {
w++
}
}
return w
}
// Update reference should be called only once in a "loop" (not Readline(), but key control loop)
func (rl *Instance) updateReferences() {
@ -32,11 +52,11 @@ func (rl *Instance) updateReferences() {
var fullLine, cPosLine int
if len(rl.currentComp) > 0 {
fullLine = len(rl.lineComp)
cPosLine = len(rl.lineComp[:rl.pos])
fullLine = getWidth(rl.lineComp)
cPosLine = getWidth(rl.lineComp[:rl.pos])
} else {
fullLine = len(rl.line)
cPosLine = len(rl.line[:rl.pos])
fullLine = getWidth(rl.line)
cPosLine = getWidth(rl.line[:rl.pos])
}
// We need the X offset of the whole line
@ -46,6 +66,10 @@ func (rl *Instance) updateReferences() {
fullRest := toEndLine % GetTermWidth()
rl.fullX = fullRest
if fullRest == 0 && fullOffset > 0 {
print("\n")
}
// Use rl.pos value to get the offset to go TO/FROM the CURRENT POSITION
lineToCursorPos := rl.promptLen + cPosLine
offsetToCursor := lineToCursorPos / GetTermWidth()
@ -75,11 +99,11 @@ func (rl *Instance) resetHelpers() {
rl.modeAutoFind = false
// Now reset all below-input helpers
rl.resetHintText()
rl.resetInfoText()
rl.resetTabCompletion()
}
// clearHelpers - Clears everything: prompt, input, hints & comps,
// clearHelpers - Clears everything: prompt, input, Infos & comps,
// and comes back at the prompt.
func (rl *Instance) clearHelpers() {
@ -97,25 +121,42 @@ func (rl *Instance) clearHelpers() {
moveCursorForwards(rl.posX)
}
// renderHelpers - pritns all components (prompt, line, hints & comps)
// renderHelpers - pritns all components (prompt, line, Infos & comps)
// and replaces the cursor to its current position. This function never
// computes or refreshes any value, except from inside the echo function.
func (rl *Instance) renderHelpers() {
// Optional, because neutral on placement
// when the instance is in this state we want it to be "below" the user's
// input for it to be aligned properly
if !rl.compConfirmWait {
rl.writeHintText()
}
rl.echo()
if rl.modeTabCompletion {
// in tab complete mode we want it to update
// when something has been selected
// (dynamic!!)
rl.getHintText()
rl.writeHintText()
} else if !rl.compConfirmWait {
// for the same reason above of wanting it below user input, do nothing here
} else {
rl.writeHintText()
}
rl.echoRightPrompt()
// Go at beginning of first line after input remainder
moveCursorDown(rl.fullY - rl.posY)
moveCursorBackwards(GetTermWidth())
// Print hints, check for any confirmation hint current.
// (do not overwrite the confirmation question hint)
// Print Infos, check for any confirmation Info current.
// (do not overwrite the confirmation question Info)
if !rl.compConfirmWait {
if len(rl.hintText) > 0 {
if len(rl.infoText) > 0 {
print("\n")
}
rl.writeHintText()
rl.writeInfoText()
moveCursorBackwards(GetTermWidth())
// Print completions and go back to beginning of this line
@ -126,17 +167,17 @@ func (rl *Instance) renderHelpers() {
}
// If we are still waiting for the user to confirm too long completions
// Immediately refresh the hints
// Immediately refresh the Infos
if rl.compConfirmWait {
print("\n")
rl.writeHintText()
rl.getHintText()
rl.writeInfoText()
rl.getInfoText()
moveCursorBackwards(GetTermWidth())
}
// Anyway, compensate for hint printout
if len(rl.hintText) > 0 {
moveCursorUp(rl.hintY)
// Anyway, compensate for Info printout
if len(rl.infoText) > 0 {
moveCursorUp(rl.infoY)
} else if !rl.compConfirmWait {
moveCursorUp(1)
} else if rl.compConfirmWait {

View File

@ -399,22 +399,22 @@ func (rl *Instance) refreshVimStatus() {
rl.updateHelpers()
}
// viHintMessage - lmorg's way of showing Vim status is to overwrite the hint.
// viInfoMessage - lmorg's way of showing Vim status is to overwrite the info.
// Currently not used, as there is a possibility to show the current Vim mode in the prompt.
func (rl *Instance) viHintMessage() {
func (rl *Instance) viInfoMessage() {
switch rl.modeViMode {
case VimKeys:
rl.hintText = []rune("-- VIM KEYS -- (press `i` to return to normal editing mode)")
rl.infoText = []rune("-- VIM KEYS -- (press `i` to return to normal editing mode)")
case VimInsert:
rl.hintText = []rune("-- INSERT --")
rl.infoText = []rune("-- INSERT --")
case VimReplaceOnce:
rl.hintText = []rune("-- REPLACE CHARACTER --")
rl.infoText = []rune("-- REPLACE CHARACTER --")
case VimReplaceMany:
rl.hintText = []rune("-- REPLACE --")
rl.infoText = []rune("-- REPLACE --")
case VimDelete:
rl.hintText = []rune("-- DELETE --")
rl.infoText = []rune("-- DELETE --")
default:
rl.getHintText()
rl.getInfoText()
}
rl.clearHelpers()

View File

@ -33,7 +33,7 @@ func (rl *Instance) viDelete(r rune) {
rl.saveBufToRegister(rl.line)
rl.clearLine()
rl.resetHelpers()
rl.getHintText()
rl.getInfoText()
case 'e':
vii := rl.getViIterations()

264
rl.go
View File

@ -5,23 +5,26 @@ import (
"io"
"strings"
"hilbish/util"
"github.com/maxlandon/readline"
"github.com/yuin/gopher-lua"
rt "github.com/arnodel/golua/runtime"
)
type lineReader struct {
rl *readline.Instance
}
var fileHist *fileHistory
var hinter *rt.Closure
var highlighter *rt.Closure
// other gophers might hate this naming but this is local, shut up
func newLineReader(prompt string, noHist bool) *lineReader {
rl := readline.NewInstance()
// we don't mind hilbish.read rl instances having completion,
// but it cant have shared history
if !noHist {
fileHist = newFileHistory()
rl.SetHistoryCtrlR("file", fileHist)
rl.SetHistoryCtrlR("History", fileHist)
rl.HistoryAutoWrite = false
}
rl.ShowVimMode = false
@ -44,9 +47,45 @@ func newLineReader(prompt string, noHist bool) *lineReader {
}
hooks.Em.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(highlighter),
rt.StringValue(string(line)), rt.IntValue(int64(pos)))
if err != nil {
fmt.Println(err)
return []rune{}
}
hintText := ""
if luaStr, ok := retVal.TryString(); ok {
hintText = luaStr
}
return []rune(hintText)
}
rl.SyntaxHighlighter = func(line []rune) string {
if highlighter == nil {
return string(line)
}
retVal, err := rt.Call1(l.MainThread(), rt.FunctionValue(highlighter),
rt.StringValue(string(line)))
if err != nil {
fmt.Println(err)
return string(line)
}
highlighted := ""
if luaStr, ok := retVal.TryString(); ok {
highlighted = luaStr
}
return highlighted
}
rl.TabCompleter = func(line []rune, pos int, _ readline.DelayedTabContext) (string, []*readline.CompletionGroup) {
ctx := string(line)
var completions []string
var compGroup []*readline.CompletionGroup
@ -75,23 +114,20 @@ func newLineReader(prompt string, noHist bool) *lineReader {
return prefix, compGroup
} else {
if completecb, ok := luaCompletions["command." + fields[0]]; ok {
luaFields := l.NewTable()
for _, f := range fields {
luaFields.Append(lua.LString(f))
luaFields := rt.NewTable()
for i, f := range fields {
luaFields.Set(rt.IntValue(int64(i + 1)), rt.StringValue(f))
}
err := l.CallByParam(lua.P{
Fn: completecb,
NRet: 1,
Protect: true,
}, lua.LString(query), lua.LString(ctx), luaFields)
// we must keep the holy 80 cols
luacompleteTable, err := rt.Call1(l.MainThread(),
rt.FunctionValue(completecb), rt.StringValue(query),
rt.StringValue(ctx), rt.TableValue(luaFields))
if err != nil {
return "", compGroup
}
luacompleteTable := l.Get(-1)
l.Pop(1)
/*
as an example with git,
completion table should be structured like:
@ -116,60 +152,98 @@ func newLineReader(prompt string, noHist bool) *lineReader {
it is the responsibility of the completer
to work on subcommands and subcompletions
*/
if cmpTbl, ok := luacompleteTable.(*lua.LTable); ok {
cmpTbl.ForEach(func(key lua.LValue, value lua.LValue) {
if key.Type() == lua.LTNumber {
// completion group
if value.Type() == lua.LTTable {
luaCmpGroup := value.(*lua.LTable)
compType := luaCmpGroup.RawGet(lua.LString("type"))
compItems := luaCmpGroup.RawGet(lua.LString("items"))
if compType.Type() != lua.LTString {
l.RaiseError("bad type name for completion (expected string, got %v)", compType.Type().String())
}
if compItems.Type() != lua.LTTable {
l.RaiseError("bad items for completion (expected table, got %v)", compItems.Type().String())
}
var items []string
itemDescriptions := make(map[string]string)
compItems.(*lua.LTable).ForEach(func(k lua.LValue, v lua.LValue) {
if k.Type() == lua.LTString {
// ['--flag'] = {'description', '--flag-alias'}
itm := v.(*lua.LTable)
items = append(items, k.String())
itemDescriptions[k.String()] = itm.RawGet(lua.LNumber(1)).String()
} else {
items = append(items, v.String())
}
})
if cmpTbl, ok := luacompleteTable.TryTable(); ok {
nextVal := rt.NilValue
for {
next, val, ok := cmpTbl.Next(nextVal)
if next == rt.NilValue {
break
}
nextVal = next
var dispType readline.TabDisplayType
switch compType.String() {
case "grid": dispType = readline.TabDisplayGrid
case "list": dispType = readline.TabDisplayList
// need special cases, will implement later
//case "map": dispType = readline.TabDisplayMap
_, ok = next.TryInt()
valTbl, okk := val.TryTable()
if !ok || !okk {
// TODO: error?
break
}
luaCompType := valTbl.Get(rt.StringValue("type"))
luaCompItems := valTbl.Get(rt.StringValue("items"))
compType, ok := luaCompType.TryString()
compItems, okk := luaCompItems.TryTable()
if !ok || !okk {
// TODO: error
break
}
var items []string
itemDescriptions := make(map[string]string)
nxVal := rt.NilValue
for {
nx, vl, _ := compItems.Next(nxVal)
if nx == rt.NilValue {
break
}
nxVal = nx
if tstr := nx.Type(); tstr == rt.StringType {
// ['--flag'] = {'description', '--flag-alias'}
nxStr, ok := nx.TryString()
vlTbl, okk := vl.TryTable()
if !ok || !okk {
// TODO: error
continue
}
compGroup = append(compGroup, &readline.CompletionGroup{
DisplayType: dispType,
Descriptions: itemDescriptions,
Suggestions: items,
TrimSlash: false,
NoSpace: true,
})
items = append(items, nxStr)
itemDescription, ok := vlTbl.Get(rt.IntValue(1)).TryString()
if !ok {
// TODO: error
continue
}
itemDescriptions[nxStr] = itemDescription
} else if tstr == rt.IntType {
vlStr, okk := vl.TryString()
if !okk {
// TODO: error
continue
}
items = append(items, vlStr)
} else {
// TODO: error
continue
}
}
})
var dispType readline.TabDisplayType
switch compType {
case "grid": dispType = readline.TabDisplayGrid
case "list": dispType = readline.TabDisplayList
// need special cases, will implement later
//case "map": dispType = readline.TabDisplayMap
}
compGroup = append(compGroup, &readline.CompletionGroup{
DisplayType: dispType,
Descriptions: itemDescriptions,
Suggestions: items,
TrimSlash: false,
NoSpace: true,
})
}
}
}
if len(compGroup) == 0 {
completions = fileComplete(query, ctx, fields)
compGroup = append(compGroup, &readline.CompletionGroup{
completions, p := fileComplete(query, ctx, fields)
fcompGroup := []*readline.CompletionGroup{{
TrimSlash: false,
NoSpace: true,
Suggestions: completions,
})
}}
return p, fcompGroup
}
}
return "", compGroup
@ -208,6 +282,13 @@ func (lr *lineReader) SetPrompt(p string) {
}
}
func (lr *lineReader) SetRightPrompt(p string) {
lr.rl.SetRightPrompt(p)
if initialized && !running {
lr.rl.RefreshPromptInPlace("")
}
}
func (lr *lineReader) AddHistory(cmd string) {
fileHist.Write(cmd)
}
@ -221,56 +302,65 @@ func (lr *lineReader) Resize() {
}
// lua module
func (lr *lineReader) Loader(L *lua.LState) *lua.LTable {
lrLua := map[string]lua.LGFunction{
"add": lr.luaAddHistory,
"all": lr.luaAllHistory,
"clear": lr.luaClearHistory,
"get": lr.luaGetHistory,
"size": lr.luaSize,
func (lr *lineReader) Loader(rtm *rt.Runtime) *rt.Table {
lrLua := map[string]util.LuaExport{
"add": {lr.luaAddHistory, 1, false},
"all": {lr.luaAllHistory, 0, false},
"clear": {lr.luaClearHistory, 0, false},
"get": {lr.luaGetHistory, 1, false},
"size": {lr.luaSize, 0, false},
}
mod := l.SetFuncs(l.NewTable(), lrLua)
mod := rt.NewTable()
util.SetExports(rtm, mod, lrLua)
return mod
}
func (lr *lineReader) luaAddHistory(l *lua.LState) int {
cmd := l.CheckString(1)
func (lr *lineReader) luaAddHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
lr.AddHistory(cmd)
return 0
return c.Next(), nil
}
func (lr *lineReader) luaSize(L *lua.LState) int {
L.Push(lua.LNumber(fileHist.Len()))
return 1
func (lr *lineReader) luaSize(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return c.PushingNext1(t.Runtime, rt.IntValue(int64(fileHist.Len()))), nil
}
func (lr *lineReader) luaGetHistory(L *lua.LState) int {
idx := L.CheckInt(1)
cmd, _ := fileHist.GetLine(idx)
L.Push(lua.LString(cmd))
func (lr *lineReader) luaGetHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
idx, err := c.IntArg(0)
if err != nil {
return nil, err
}
return 0
cmd, _ := fileHist.GetLine(int(idx))
return c.PushingNext1(t.Runtime, rt.StringValue(cmd)), nil
}
func (lr *lineReader) luaAllHistory(L *lua.LState) int {
tbl := L.NewTable()
func (lr *lineReader) luaAllHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
tbl := rt.NewTable()
size := fileHist.Len()
for i := 1; i < size; i++ {
cmd, _ := fileHist.GetLine(i)
tbl.Append(lua.LString(cmd))
tbl.Set(rt.IntValue(int64(i)), rt.StringValue(cmd))
}
L.Push(tbl)
return 0
return c.PushingNext1(t.Runtime, rt.TableValue(tbl)), nil
}
func (lr *lineReader) luaClearHistory(l *lua.LState) int {
return 0
func (lr *lineReader) luaClearHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
fileHist.clear()
return c.Next(), nil
}

56
runnermode.go 100644
View File

@ -0,0 +1,56 @@
package main
import (
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
)
func runnerModeLoader(rtm *rt.Runtime) *rt.Table {
exports := map[string]util.LuaExport{
"sh": {shRunner, 1, false},
"lua": {luaRunner, 1, false},
"setMode": {hlrunnerMode, 1, false},
}
mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
return mod
}
func shRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
input, exitCode, err := handleSh(cmd)
var luaErr rt.Value = rt.NilValue
if err != nil {
luaErr = rt.StringValue(err.Error())
}
return c.PushingNext(t.Runtime, rt.StringValue(input), rt.IntValue(int64(exitCode)), luaErr), nil
}
func luaRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
input, exitCode, err := handleLua(cmd)
var luaErr rt.Value = rt.NilValue
if err != nil {
luaErr = rt.StringValue(err.Error())
}
return c.PushingNext(t.Runtime, rt.StringValue(input), rt.IntValue(int64(exitCode)), luaErr), nil
}

106
timer.go 100644
View File

@ -0,0 +1,106 @@
package main
import (
"errors"
"fmt"
"os"
"time"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
)
type timerType int64
const (
timerInterval timerType = iota
timerTimeout
)
type timer struct{
id int
typ timerType
running bool
dur time.Duration
fun *rt.Closure
th *timerHandler
ticker *time.Ticker
channel chan bool
}
func (t *timer) start() error {
if t.running {
return errors.New("timer is already running")
}
t.running = true
t.th.running++
t.ticker = time.NewTicker(t.dur)
go func() {
for {
select {
case <-t.ticker.C:
_, err := rt.Call1(l.MainThread(), rt.FunctionValue(t.fun))
if err != nil {
fmt.Fprintln(os.Stderr, "Error in function:\n", err)
t.stop()
}
// only run one for timeout
if t.typ == timerTimeout {
t.stop()
}
case <-t.channel:
t.ticker.Stop()
return
}
}
}()
return nil
}
func (t *timer) stop() error {
if !t.running {
return errors.New("timer not running")
}
t.channel <- true
t.running = false
t.th.running--
return nil
}
func (t *timer) luaStart(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
err := t.start()
if err != nil {
return nil, err
}
return c.Next(), nil
}
func (t *timer) luaStop(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
err := t.stop()
if err != nil {
return nil, err
}
return c.Next(), nil
}
func (t *timer) lua() rt.Value {
tExports := map[string]util.LuaExport{
"start": {t.luaStart, 0, false},
"stop": {t.luaStop, 0, false},
}
luaTimer := rt.NewTable()
util.SetExports(l, luaTimer, tExports)
luaTimer.Set(rt.StringValue("type"), rt.IntValue(int64(t.typ)))
luaTimer.Set(rt.StringValue("running"), rt.BoolValue(t.running))
luaTimer.Set(rt.StringValue("duration"), rt.IntValue(int64(t.dur / time.Millisecond)))
return rt.TableValue(luaTimer)
}

102
timerhandler.go 100644
View File

@ -0,0 +1,102 @@
package main
import (
"sync"
"time"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
)
var timers *timerHandler
type timerHandler struct {
mu *sync.RWMutex
timers map[int]*timer
latestID int
running int
}
func newTimerHandler() *timerHandler {
return &timerHandler{
timers: make(map[int]*timer),
latestID: 0,
mu: &sync.RWMutex{},
}
}
func (th *timerHandler) create(typ timerType, dur time.Duration, fun *rt.Closure) *timer {
th.mu.Lock()
defer th.mu.Unlock()
th.latestID++
t := &timer{
typ: typ,
fun: fun,
dur: dur,
channel: make(chan bool, 1),
th: th,
id: th.latestID,
}
th.timers[th.latestID] = t
return t
}
func (th *timerHandler) 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) {
if err := c.CheckNArgs(3); err != nil {
return nil, err
}
timerTypInt, err := c.IntArg(0)
if err != nil {
return nil, err
}
ms, err := c.IntArg(1)
if err != nil {
return nil, err
}
cb, err := c.ClosureArg(2)
if err != nil {
return nil, err
}
timerTyp := timerType(timerTypInt)
tmr := th.create(timerTyp, time.Duration(ms) * time.Millisecond, cb)
return c.PushingNext1(t.Runtime, tmr.lua()), nil
}
func (th *timerHandler) luaGet(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
id, err := c.IntArg(0)
if err != nil {
return nil, err
}
t := th.get(int(id))
if t != nil {
return c.PushingNext1(thr.Runtime, t.lua()), nil
}
return c.Next(), nil
}
func (th *timerHandler) loader(rtm *rt.Runtime) *rt.Table {
thExports := map[string]util.LuaExport{
"create": {th.luaCreate, 3, false},
"get": {th.luaGet, 1, false},
}
luaTh := rt.NewTable()
util.SetExports(rtm, luaTh, thExports)
return luaTh
}

19
util/export.go 100644
View File

@ -0,0 +1,19 @@
package util
import (
rt "github.com/arnodel/golua/runtime"
)
// LuaExport represents a Go function which can be exported to Lua.
type LuaExport struct {
Function rt.GoFunctionFunc
ArgNum int
Variadic bool
}
// SetExports puts the Lua function exports in the table.
func SetExports(rtm *rt.Runtime, tbl *rt.Table, exports map[string]LuaExport) {
for name, export := range exports {
rtm.SetEnvGoFunc(tbl, name, export.Function, export.ArgNum, export.Variadic)
}
}

View File

@ -1,32 +1,120 @@
package util
import "github.com/yuin/gopher-lua"
import (
"bufio"
"io"
"os"
rt "github.com/arnodel/golua/runtime"
)
// Document adds a documentation string to a module.
// It is accessible via the __doc metatable.
func Document(L *lua.LState, module lua.LValue, doc string) {
mt := L.GetMetatable(module)
if mt == lua.LNil {
mt = L.NewTable()
L.SetMetatable(module, mt)
func Document(module *rt.Table, doc string) {
mt := module.Metatable()
if mt == nil {
mt = rt.NewTable()
module.SetMetatable(mt)
}
L.SetField(mt, "__doc", lua.LString(doc))
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(L *lua.LState, module lua.LValue, field string, value lua.LValue, doc string) {
mt := L.GetMetatable(module)
if mt == lua.LNil {
mt = L.NewTable()
docProp := L.NewTable()
L.SetField(mt, "__docProp", docProp)
func SetField(rtm *rt.Runtime, module *rt.Table, field string, value rt.Value, doc string) {
// TODO: ^ rtm isnt needed, i should remove it
mt := module.Metatable()
if mt == nil {
mt = rt.NewTable()
docProp := rt.NewTable()
mt.Set(rt.StringValue("__docProp"), rt.TableValue(docProp))
L.SetMetatable(module, mt)
module.SetMetatable(mt)
}
docProp := L.GetTable(mt, lua.LString("__docProp"))
docProp := mt.Get(rt.StringValue("__docProp"))
L.SetField(docProp, field, lua.LString(doc))
L.SetField(module, field, value)
docProp.AsTable().Set(rt.StringValue(field), rt.StringValue(doc))
module.Set(rt.StringValue(field), value)
}
// DoString runs the code string in the Lua runtime.
func DoString(rtm *rt.Runtime, code string) error {
chunk, err := rtm.CompileAndLoadLuaChunk("<string>", []byte(code), rt.TableValue(rtm.GlobalEnv()))
if chunk != nil {
_, err = rt.Call1(rtm.MainThread(), rt.FunctionValue(chunk))
}
return err
}
// DoFile runs the contents of the file in the Lua runtime.
func DoFile(rtm *rt.Runtime, path string) error {
f, err := os.Open(path)
defer f.Close()
if err != nil {
return err
}
reader := bufio.NewReader(f)
c, err := reader.ReadByte()
if err != nil && err != io.EOF {
return err
}
// unread so a char won't be missing
err = reader.UnreadByte()
if err != nil {
return err
}
var buf []byte
if c == byte('#') {
// shebang - skip that line
_, err := reader.ReadBytes('\n')
if err != nil && err != io.EOF {
return err
}
buf = []byte{'\n'}
}
for {
line, err := reader.ReadBytes('\n')
if err != nil {
if err == io.EOF {
break
}
return err
}
buf = append(buf, line...)
}
clos, err := rtm.LoadFromSourceOrCode(path, buf, "bt", rt.TableValue(rtm.GlobalEnv()), false)
if clos != nil {
_, err = rt.Call1(rtm.MainThread(), rt.FunctionValue(clos))
}
return err
}
// HandleStrCallback handles function parameters for Go functions which take
// a string and a closure.
func HandleStrCallback(t *rt.Thread, c *rt.GoCont) (string, *rt.Closure, error) {
if err := c.CheckNArgs(2); err != nil {
return "", nil, err
}
name, err := c.StringArg(0)
if err != nil {
return "", nil, err
}
cb, err := c.ClosureArg(1)
if err != nil {
return "", nil, err
}
return name, cb, err
}

View File

@ -2,8 +2,7 @@ package main
// String vars that are free to be changed at compile time
var (
version = "v1.0.4"
defaultConfDir = "" // ~ will be substituted for home, path for user's default config
version = "v2.0.0"
defaultHistDir = ""
commonRequirePaths = "';./libs/?/init.lua;./?/init.lua;./?/?.lua'"

View File

@ -17,4 +17,5 @@ var (
dataDir = "/usr/local/share/hilbish"
preloadPath = dataDir + "/prelude/init.lua"
sampleConfPath = dataDir + "/.hilbishrc.lua" // Path to default/sample config
defaultConfDir = getenv("XDG_CONFIG_HOME", "~/.config")
)

View File

@ -17,4 +17,5 @@ var (
dataDir = "/usr/share/hilbish"
preloadPath = dataDir + "/prelude/init.lua"
sampleConfPath = dataDir + "/.hilbishrc.lua" // Path to default/sample config
defaultConfDir = ""
)

View File

@ -11,4 +11,5 @@ var (
dataDir = "~\\Appdata\\Roaming\\Hilbish" // ~ and \ gonna cry?
preloadPath = dataDir + "\\prelude\\init.lua"
sampleConfPath = dataDir + "\\hilbishrc.lua" // Path to default/sample config
defaultConfDir = ""
)