-
-
Notifications
You must be signed in to change notification settings - Fork 303
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce pkg/etk, a new TUI framework.
The framework has the following interesting features: - It has a first-class Elvish binding with the same expressive power as the Go version of the framework. - It has an immediate-mode API, meaning that a component is represented by a function that gets invoked every time the state changes. For more on the motivation and tradeoffs of the design, see the slidedeck in website/slides/draft-etk.md. This package will eventually replace pkg/cli/tk as Elvish's internal TUI framework, and Elvish's TUI will be rewritten to use it.
- Loading branch information
Showing
43 changed files
with
3,485 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
use etk | ||
|
||
# mkdir prompt - what can be done today: | ||
var w | ||
set w = (edit:new-codearea [&prompt=(styled 'mkdir:' inverse)' ' &on-submit={ mkdir (edit:get-state $w)[buffer][content] }]) | ||
edit:push-addon $w | ||
|
||
# mkdir prompt - slightly cleaned up version | ||
edit:push-addon (etk:new-codearea [&prompt='mkdir: ' &on-submit={|s| mkdir $s[buffer][content] }]) | ||
|
||
# mkdir prompt - state management version | ||
var dirname | ||
edit:push-addon { | ||
etk:textbox [&prompt='mkdir: ' &on-submit={ mkdir $dirname }] ^ | ||
[&buffer=[&content=(bind dirname)]] | ||
} | ||
|
||
# Temperature conversion | ||
var c = '' | ||
etk:run-app { | ||
var f = (/ (- $c 32) 1.8) | ||
etk:vbox [&children=[ | ||
(etk:textbox [&prompt='input: '] [&buffer=[&content=(bind c)]]) | ||
(etk:label $c' ℉ = '$f' ℃') | ||
]] | ||
} | ||
|
||
# Elvish configuration helper | ||
var tasks = [ | ||
[&name='Use readline binding' | ||
&detail='Readline binding enables keys like Ctrl-N, Ctrl-F' | ||
&eval-code='' | ||
&rc-code='use readline-binding'] | ||
|
||
[&name='Install Carapace' | ||
&detail='Carapace provides completions.' | ||
&eval-code='brew install carapace' | ||
&rc-code='eval (carapace init elvish)'] | ||
] | ||
|
||
fn execute-task {|task| | ||
eval $task[eval-code] | ||
eval $task[rc-code] | ||
echo $task[rc-code] >> $runtime:rc-file | ||
} | ||
|
||
var i = (num 0) | ||
etk:run-app { | ||
etk:hbox [&children=[ | ||
(etk:list [&items=$tasks &display={|t| put $t[name]} &on-submit=$execute-task~] ^ | ||
[&selected=(bind i)]) | ||
(etk:label $tasks[i][detail]) | ||
]] | ||
} | ||
|
||
# Markdown-driven presentation | ||
var filename = 'a.md' | ||
var @slides = (slurp < $filename | | ||
re:split '\n {0,3}((?:-[ \t]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})\n' (one)) | ||
|
||
var i = (num 0) | ||
etk:run-app { | ||
etk:vbox [ | ||
&binding=[&Left={|_| set i = (- $i 1) } &Right={|_| set i = (+ $i 1) }] | ||
&children=[ | ||
(etk:label $slides[i]) | ||
(etk:label (+ 1 $i)/(count $slides)) | ||
] | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package edit | ||
|
||
import ( | ||
"src.elv.sh/pkg/cli" | ||
"src.elv.sh/pkg/eval" | ||
) | ||
|
||
func initCustomWidgetAPI(app cli.App, nb eval.NsBuilder) { | ||
nb.AddGoFns(map[string]any{ | ||
"push-addon": app.PushAddon, | ||
"pop-addon": app.PopAddon, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
package etk | ||
|
||
/* | ||
// Ns provides the etk: module, an Elvish binding for this TUI framework. | ||
var Ns = eval.BuildNsNamed("etk"). | ||
AddVars(map[string]vars.Var{ | ||
"codearea": vars.NewReadOnly(CodeArea), | ||
"listbox": vars.NewReadOnly(ListBox), | ||
}). | ||
AddGoFns(map[string]any{ | ||
"comp": comp, | ||
"vbox": vbox, | ||
"handle": handle, | ||
"adapt-to-widget": adaptToWidget, | ||
"text-buffer": func(content string, dot int) tk.CodeBuffer { | ||
return tk.CodeBuffer{Content: content, Dot: dot} | ||
}, | ||
}).Ns() | ||
func comp(fm *eval.Frame, fn eval.Callable) Comp { | ||
return func(c Context) (View, React) { | ||
subcompViews := map[string]View{} | ||
subcompReacts := map[string]React{} | ||
var this = eval.BuildNs().AddVars(map[string]vars.Var{ | ||
"state": stateSubTreeVar(c), | ||
"subcomp": vars.FromGet(func() any { | ||
m := vals.EmptyMap | ||
for k, v := range subcomps { | ||
m = m.Assoc(k, v) | ||
} | ||
return m | ||
}), | ||
}).AddGoFns(map[string]any{ | ||
"state": func(name string, _eq string, init any) { | ||
State(c, name, init) | ||
}, | ||
"subcomp": func(name string, f Comp, setStatesMap vals.Map) (Scene, error) { | ||
setStates, err := convertSetStates(setStatesMap) | ||
if err != nil { | ||
return Scene{}, err | ||
} | ||
el := c.Subcomp(name, WithStates(f, setStates...)) | ||
subcomps[name] = el | ||
return el, nil | ||
}, | ||
}).Ns() | ||
p1, getOut, err := eval.ValueCapturePort() | ||
if err != nil { | ||
return errElement(err) | ||
} | ||
err = fm.Evaler.Call(fn, eval.CallCfg{Args: []any{this}}, | ||
eval.EvalCfg{Ports: []*eval.Port{nil, p1, nil}}) | ||
if err != nil { | ||
return errElement(err) | ||
} | ||
outs := getOut() | ||
if len(outs) != 1 { | ||
return errElement(fmt.Errorf("should only have one output")) | ||
} | ||
el, ok := outs[0].(Scene) | ||
if !ok { | ||
return errElement(fmt.Errorf("output should be element")) | ||
} | ||
return el | ||
} | ||
} | ||
type stateSubTreeVar Context | ||
func (v stateSubTreeVar) Get() any { | ||
return getPath(*v.state, v.path) | ||
} | ||
func (v stateSubTreeVar) Set(val any) error { | ||
valMap, ok := val.(vals.Map) | ||
if !ok { | ||
return fmt.Errorf("must be map") | ||
} | ||
*v.state = assocPath(*v.state, v.path, valMap) | ||
return nil | ||
} | ||
func convertSetStates(m vals.Map) ([]any, error) { | ||
var setStates []any | ||
for it := m.Iterator(); it.HasElem(); it.Next() { | ||
k, v := it.Elem() | ||
name, ok := k.(string) | ||
if !ok { | ||
return nil, fmt.Errorf("key should be string") | ||
} | ||
setStates = append(setStates, name, v) | ||
} | ||
return setStates, nil | ||
} | ||
func vbox(fm *eval.Frame, rowsList vals.List, propsMap vals.Map) Scene { | ||
var rows []View | ||
for it := rowsList.Iterator(); it.HasElem(); it.Next() { | ||
elem, ok := it.Elem().(Scene) | ||
if !ok { | ||
return errElement(fmt.Errorf("vbox needs elements")) | ||
} | ||
rows = append(rows, elem.View) | ||
} | ||
focusAny, ok := propsMap.Index("focus") | ||
if !ok { | ||
return errElement(fmt.Errorf("vbox needs focus")) | ||
} | ||
focus, ok := focusAny.(int) | ||
if !ok { | ||
return errElement(fmt.Errorf("vbox needs int focus")) | ||
} | ||
handlerAny, ok := propsMap.Index("handler") | ||
if !ok { | ||
return errElement(fmt.Errorf("vbox needs handler")) | ||
} | ||
handler, ok := handlerAny.(eval.Callable) | ||
if !ok { | ||
return errElement(fmt.Errorf("vbox needs callable handler")) | ||
} | ||
return VBoxView{Rows: rows, Focus: focus}.WithHandler(func(e term.Event) Action { | ||
s := "" | ||
if ke, ok := e.(term.KeyEvent); ok { | ||
s = ui.Key(ke).String() | ||
} | ||
p1, getOut, err := eval.ValueCapturePort() | ||
if err != nil { | ||
// How do I indicate error here 😨 | ||
Notify(ui.T(fmt.Sprintf("value capture port error: %s", err))) | ||
return Errored | ||
} | ||
err = fm.Evaler.Call(handler, eval.CallCfg{Args: []any{e, s}}, | ||
eval.EvalCfg{Ports: []*eval.Port{nil, p1, nil}}) | ||
if err != nil { | ||
// How do I indicate error here 😨 | ||
var sb strings.Builder | ||
diag.ShowError(&sb, err) | ||
Notify(ui.T("handler exception")) | ||
Notify(ui.ParseSGREscapedText(sb.String())) | ||
return Errored | ||
} | ||
for _, out := range getOut() { | ||
if action, ok := out.(Action); ok { | ||
return action | ||
} | ||
} | ||
// TODO: Error when there's no than one Action output | ||
return Errored | ||
}) | ||
} | ||
func errElement(err error) Scene { | ||
return Text(ui.T(err.Error(), ui.FgRed)).WithHandler(func(term.Event) Action { return Unused }) | ||
} | ||
func handle(el Scene, ev term.Event) Action { | ||
return el.React(ev) | ||
} | ||
func adaptToWidget(f func(Context) Scene) tk.Widget { | ||
return AdaptToWidget(f) | ||
} | ||
*/ |
Oops, something went wrong.