Compare commits

...

7 Commits

Author SHA1 Message Date
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
11 changed files with 331 additions and 38 deletions

50
api.go
View File

@ -131,6 +131,11 @@ Check out the {blue}{bold}guide{reset} command to get started.
jobModule := jobs.loader(rtm) jobModule := jobs.loader(rtm)
util.Document(jobModule, "(Background) job interface.") util.Document(jobModule, "(Background) job interface.")
mod.Set(rt.StringValue("jobs"), rt.TableValue(jobModule)) 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))
return rt.TableValue(mod), nil return rt.TableValue(mod), nil
} }
@ -466,8 +471,10 @@ func hlgoro(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// timeout(cb, time) // timeout(cb, time)
// Runs the `cb` function after `time` in milliseconds // Runs the `cb` function after `time` in milliseconds
// Returns a `timer` object (see `doc timers`).
// --- @param cb function // --- @param cb function
// --- @param time number // --- @param time number
// --- @return table
func hltimeout(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { func hltimeout(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(2); err != nil { if err := c.CheckNArgs(2); err != nil {
return nil, err return nil, err
@ -481,21 +488,19 @@ func hltimeout(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return nil, err return nil, err
} }
timeout := time.Duration(ms) * time.Millisecond interval := time.Duration(ms) * time.Millisecond
time.Sleep(timeout) timer := timers.create(timerTimeout, interval, cb)
timer.start()
_, err = rt.Call1(l.MainThread(), rt.FunctionValue(cb))
if err != nil { return c.PushingNext1(t.Runtime, timer.lua()), nil
return nil, err
}
return c.Next(), nil
} }
// interval(cb, time) // 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 cb function
// --- @param time number // --- @param time number
// --- @return table
func hlinterval(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { func hlinterval(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(2); err != nil { if err := c.CheckNArgs(2); err != nil {
return nil, err return nil, err
@ -508,29 +513,12 @@ func hlinterval(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
interval := time.Duration(ms) * time.Millisecond interval := time.Duration(ms) * time.Millisecond
timer := timers.create(timerInterval, interval, cb)
timer.start()
ticker := time.NewTicker(interval) return c.PushingNext1(t.Runtime, timer.lua()), nil
stop := make(chan rt.Value)
go func() {
for {
select {
case <-ticker.C:
_, err := rt.Call1(l.MainThread(), rt.FunctionValue(cb))
if err != nil {
fmt.Fprintln(os.Stderr, "Error in interval function:\n\n", err)
stop <- rt.BoolValue(true) // stop the interval
}
case <-stop:
ticker.Stop()
return
}
}
}()
// TODO: return channel
return c.Next(), nil
} }
// complete(scope, cb) // complete(scope, cb)

View File

@ -28,7 +28,8 @@ which will be used for the hint.
inputMode(mode) > Sets the input mode for Hilbish's line reader. Accepts either emacs for vim 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` multiprompt(str) > Changes the continued line prompt to `str`
@ -56,6 +57,7 @@ sh, and lua. It also accepts a function, to which if it is passed one
will call it to execute user input instead. will call it to execute user input instead.
timeout(cb, time) > Runs the `cb` function after `time` in milliseconds 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 which(binName) > Searches for an executable called `binName` in the directories of $PATH

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

@ -51,9 +51,11 @@ function hilbish.hinter(cb) end
--- @param mode string --- @param mode string
function hilbish.inputMode(mode) end 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 cb function
--- @param time number --- @param time number
--- @return table
function hilbish.interval(cb, time) end function hilbish.interval(cb, time) end
--- Changes the continued line prompt to `str` --- Changes the continued line prompt to `str`
@ -94,8 +96,10 @@ function hilbish.run(cmd) end
function hilbish.runnerMode(mode) end function hilbish.runnerMode(mode) end
--- Runs the `cb` function after `time` in milliseconds --- Runs the `cb` function after `time` in milliseconds
--- Returns a `timer` object (see `doc timers`).
--- @param cb function --- @param cb function
--- @param time number --- @param time number
--- @return table
function hilbish.timeout(cb, time) end function hilbish.timeout(cb, time) end
--- Searches for an executable called `binName` in the directories of $PATH --- Searches for an executable called `binName` in the directories of $PATH

View File

@ -125,6 +125,9 @@ func handleSh(cmdString string) (uint8, error) {
if err != nil { if err != nil {
// If input is incomplete, start multiline prompting // If input is incomplete, start multiline prompting
if syntax.IsIncomplete(err) { if syntax.IsIncomplete(err) {
if !interactive {
return 126, err
}
for { for {
cmdString, err = continuePrompt(strings.TrimSuffix(cmdString, "\\")) cmdString, err = continuePrompt(strings.TrimSuffix(cmdString, "\\"))
if err != nil { if err != nil {

14
main.go
View File

@ -145,6 +145,7 @@ func main() {
text := scanner.Text() text := scanner.Text()
runInput(text, true) runInput(text, true)
} }
exit(0)
} }
if *cmdflag != "" { if *cmdflag != "" {
@ -161,9 +162,9 @@ func main() {
err := util.DoFile(l, getopt.Arg(0)) err := util.DoFile(l, getopt.Arg(0))
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
os.Exit(1) exit(1)
} }
os.Exit(0) exit(0)
} }
initialized = true initialized = true
@ -299,3 +300,12 @@ func contains(s []string, e string) bool {
} }
return false return false
} }
func exit(code int) {
// wait for all timers to finish before exiting
for {
if timers.running == 0 {
os.Exit(code)
}
}
}

View File

@ -49,6 +49,8 @@ var (
seqEndSc = string([]byte{27, 91, 52, 126}) seqEndSc = string([]byte{27, 91, 52, 126})
seqDelete = string([]byte{27, 91, 51, 126}) seqDelete = string([]byte{27, 91, 51, 126})
seqDelete2 = string([]byte{27, 91, 80}) seqDelete2 = string([]byte{27, 91, 80})
seqCtrlDelete = string([]byte{27, 91, 51, 59, 53, 126})
seqCtrlDelete2 = string([]byte{27, 91, 77})
seqShiftTab = string([]byte{27, 91, 90}) seqShiftTab = string([]byte{27, 91, 90})
seqAltQuote = string([]byte{27, 34}) // Added for showing registers ^[" seqAltQuote = string([]byte{27, 34}) // Added for showing registers ^["
seqAltR = string([]byte{27, 114}) // Used for alternative history seqAltR = string([]byte{27, 114}) // Used for alternative history

View File

@ -182,3 +182,27 @@ func (rl *Instance) deleteToEnd() {
// Keep everything before the cursor // Keep everything before the cursor
rl.line = rl.line[:rl.pos] rl.line = rl.line[:rl.pos]
} }
func (rl *Instance) emacsForwardWord(tokeniser tokeniser) (adjust int) {
// when emacs has more specific stuff, move this in a file with then
split, index, pos := tokeniser(rl.line, rl.pos)
if len(split) == 0 {
return
}
word := rTrimWhiteSpace(split[index])
switch {
case len(split) == 0:
return
case index == len(split)-1 && pos >= len(word)-1:
return
case pos >= len(word)-1:
word = rTrimWhiteSpace(split[index+1])
adjust = len(split[index]) - pos
adjust += len(word)
default:
adjust = len(word) - pos
}
return
}

View File

@ -170,9 +170,18 @@ func (rl *Instance) Readline() (string, error) {
rl.clearHelpers() rl.clearHelpers()
return "", CtrlC return "", CtrlC
case charEOF: case charEOF: // ctrl d
rl.clearHelpers() if len(rl.line) == 0 {
return "", EOF rl.clearHelpers()
return "", EOF
}
if rl.modeTabFind {
rl.backspaceTabFind()
} else {
if (rl.pos < len(rl.line)) {
rl.deleteBackspace(true)
}
}
// Clear screen // Clear screen
case charCtrlL: case charCtrlL:
@ -777,6 +786,19 @@ func (rl *Instance) escapeSeq(r []rune) {
rl.viDeleteByAdjust(rl.viJumpB(tokeniseLine)) rl.viDeleteByAdjust(rl.viJumpB(tokeniseLine))
rl.updateHelpers() rl.updateHelpers()
case seqCtrlDelete, seqCtrlDelete2:
if rl.modeTabCompletion {
rl.resetVirtualComp(false)
}
// This is only available in Insert mode
if rl.modeViMode != VimInsert {
return
}
rl.saveToRegister(rl.emacsForwardWord(tokeniseLine))
// vi delete, emacs forward, funny huh
rl.viDeleteByAdjust(rl.emacsForwardWord(tokeniseLine))
rl.updateHelpers()
default: default:
if rl.modeTabFind { if rl.modeTabFind {
return return

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
}