Compare commits

...

17 Commits

Author SHA1 Message Date
Daniel Barlow da06309e75 fix hardcoded visit-buffer param default 2022-12-31 16:46:34 +00:00
Daniel Barlow daa244c0b3 add switch-to-buffer command 2022-12-31 16:46:34 +00:00
Daniel Barlow 69d8aa4131 detect and discard modifier-only key events
for recognising key sequences, we only want  to look at
events containing non-modifier keystrokes
2022-12-31 16:46:34 +00:00
Daniel Barlow cc2caae372 all commands are called with frame as well as declared params
the commander is per-frame, so this is in the nature of a
"context" parameter
2022-12-31 16:46:34 +00:00
Daniel Barlow b8d86c65cb invoke command.function with completion value not string
this means a function that wants a buffer (for example) gets called
with the actual buffer not the buffer name
2022-12-31 16:46:34 +00:00
Daniel Barlow e4ed51e137 add gtk stuff to show completions as user types 2022-12-31 16:46:34 +00:00
Daniel Barlow 36edd12c6e remove duplicated code 2022-12-31 16:46:34 +00:00
Daniel Barlow fb834f496c rename on-input -> on-activate
it's only called when RET is pressed, not during editing
2022-12-31 16:46:34 +00:00
Daniel Barlow 28080a1387 define-command parameters now specify a 'completer' function 2022-12-31 16:46:27 +00:00
Daniel Barlow 0ea9ba1b92 remove dead code 2022-12-29 17:40:09 +00:00
Daniel Barlow 6454620307 ignore generated file 2022-12-29 17:34:51 +00:00
Daniel Barlow 454f9ae094 use overmind to restart dying test runners 2022-12-29 17:21:50 +00:00
Daniel Barlow 5069e6aff1 move command state into per-frame table
lots of dependent changes here, unfortunately

- get rid of Buffer.current (it's per-frame now)
- in define-command, functions providing default parameter values
   now accept frame as parameter
- in keymaps, parameter values are functions that accept frame as
   parameter
2022-12-27 12:25:50 +00:00
Daniel Barlow dea9d925a5 add lume module 2022-12-27 12:23:24 +00:00
Daniel Barlow dd139c9796 express keymap bindings as [command-name args]
instead of using functions directly. This is so that the
appropriate frame-relevant commander can be used to execure
the command, but also makes the keymaps a little less opaque
2022-12-26 16:53:41 +00:00
Daniel Barlow a5612fce2a some musing about switch-to-buffer 2022-12-26 14:48:53 +00:00
Daniel Barlow bbd67b3a16 make entry widget a file-scoped local 2022-12-26 14:48:53 +00:00
12 changed files with 269 additions and 134 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
doc/index.html

1
.overmind.env Normal file
View File

@ -0,0 +1 @@
OVERMIND_AUTO_RESTART=test

View File

@ -2,4 +2,4 @@ doc/index.html: doc/index.md
pandoc -t html -f gfm < $< > $@
watch:
while true ; do ( find . -type f | entr -d sh test/run.sh ) ;done
find . -type f | entr -c -d sh test/run.sh

View File

@ -33,6 +33,14 @@
(let [b (new-buffer name)]
(tset buffers name b)
b))
:current (fn [] (let [k (next buffers)] (. buffers k)))
:find (fn [term] (. buffers term))
;; will rename this to "find" once we've got rid of the
;; only remaining call to the existing Buffer.find
:match (fn [s] (collect [name buffer (pairs buffers)]
(if (string.find name s)
(values name buffer))))
:next (fn [buffer]
(let [n (or (next buffers buffer.name) (next buffers))]
(. buffers n)))
})

View File

@ -1,23 +1,16 @@
(local { : Gtk } (require :lgi))
(local { : view } (require :fennel))
(local lume (require :lume))
(local commands {})
(local Buffer (require :buffer))
(fn by-pairs [a]
(let [iter (fn [_ a]
(match a
[k v & rest] (values rest k v)
_ nil))]
(values iter a a)))
(fn define-command [name function ordered-params]
(fn define-command [name ordered-params function]
;; required parameter names and default arguments
(let [param-names (icollect [_ name val (by-pairs ordered-params)]
name)
params (collect [_ name val (by-pairs ordered-params)]
(values name val))
(let [param-names (icollect [_ [name] (pairs ordered-params)] name)
params (collect [_ [name completer default] (pairs ordered-params)]
(values name {: completer : default}))
v {: name
: function
: param-names
@ -26,15 +19,27 @@
(define-command
"quit-browser"
#(Gtk.main_quit) [])
[]
#(Gtk.main_quit))
(define-command
"switch-to-buffer"
[[:buffer
Buffer.match
#(. (Buffer.next $1.buffer) :name)]
]
(fn [{:frame frame :buffer buffer}]
(frame:show-buffer buffer)))
(define-command
"visit-location"
[[:buffer
Buffer.match
#($1.buffer.name)]
[:url #(doto {} (tset $1 $1)) #(do "http://www.example.com")]
]
(fn [{:url url :buffer buffer}]
(let [b (Buffer.find buffer)] (: b :visit url)))
[:buffer (fn [] (. (Buffer.current) :name))
:url #(do "http://www.example.com")
])
(buffer:visit url)))
(fn find-command [name]
(. commands name))
@ -46,10 +51,6 @@
:this-param nil
})
(var state default-state)
(fn reset-state []
(set state default-state))
(fn next-param [command params]
(accumulate [v nil
_ k (ipairs command.param-names)
@ -59,8 +60,9 @@
(fn invoke-command [command params]
(command.function params))
(fn next-action [state input-string]
(let [state-for-next-param
(fn next-action [self input-string]
(let [state self.state
state-for-next-param
(fn [c params]
(match (next-param c params)
k1 {
@ -69,7 +71,9 @@
:collected-params params
:active true
}
_ (do (invoke-command c params) {:active false})))]
_ (let [params (lume.extend {} {:frame self.frame} params)]
(invoke-command c params)
{:active false})))]
(match state
{:active false} state
@ -86,8 +90,10 @@
})
{:command c :this-param k :collected-params p}
(do
(tset p k input-string)
(let [{ : completer} (. c.params k)
vals (completer input-string)
value (. vals input-string)]
(tset p k value)
(state-for-next-param c p))
{:command c :this-param nil :collected-params p}
@ -98,75 +104,102 @@
state)
)))
(fn on-input [str]
(let [s (next-action state str)
(fn on-activate [self str]
(let [s (next-action self str)
param (if s.active (. (. s.command :params) s.this-param))]
(set state s)
(set self.state s)
{
:active s.active
:error s.error
:prompt (if s.active (or s.this-param "Command?" "") "")
:default (and param (param))
:default (and param (param.default self.frame))
}))
(local prompt (Gtk.Label { :label ""}))
(fn update-widget-state [{ : entry : completions-widget : prompt} result]
(set prompt.label (or result.prompt ""))
(set entry.sensitive result.active)
(if (not result.active)
(completions-widget:hide))
(set entry.text (or result.default result.error "")))
(fn update-widget-state [w result]
(set prompt.label (or result.prompt ""))
(set w.sensitive result.active)
(set w.text
(or result.default result.error "")))
(fn on-input [self str]
(match self.state
{:command c :this-param param-name}
(let [parent self.completions-widget
{ : completer} (. c.params param-name)
completions (completer str)]
(parent:foreach #(parent:remove $1))
(each [text _w (pairs completions)]
(parent:add (Gtk.Button {
:label text
:on_clicked
#(update-widget-state self (self:on-activate text))
})))
(parent:show_all)
)))
(local widget
(let [w (Gtk.Entry {
:sensitive false
})]
(tset w :on_activate
(fn [event]
(update-widget-state w (on-input event.text))))
w))
(local box
(let [box
(Gtk.Box {
:orientation Gtk.Orientation.HORIZONTAL
})]
(box:pack_start prompt false false 15)
(box:pack_start widget true true 5)
box))
(fn activate []
(fn activate [{: state : entry : prompt}]
(tset state :active true)
(set widget.sensitive true)
(set widget.text "")
(set entry.sensitive true)
(set entry.text "")
(set prompt.label (or state.this-param "Command" ""))
(widget:grab_focus)
(entry:grab_focus)
state)
(fn invoke-interactively [name params]
(fn invoke-interactively [self name params]
(let [c (find-command name)
supplied-params (collect [k v (pairs params)]
(values k (v self.frame)))
s {
:active true
:command c
:collected-params params
:collected-params supplied-params
}]
(set state s)
(let [r (on-input nil)]
(update-widget-state widget r)
(widget:grab_focus)
(set self.state s)
(let [r (self:on-activate nil)]
(update-widget-state self r)
(self.entry:grab_focus)
r)))
(fn active? [] state.active)
(fn new-commander [frame]
(let [entry (Gtk.Entry {:sensitive false })
prompt (Gtk.Label { :label ""})
box (Gtk.Box {
:orientation Gtk.Orientation.VERTICAL
})
hbox (Gtk.Box {
:orientation Gtk.Orientation.HORIZONTAL
})
completions (Gtk.FlowBox)
self {
:state default-state
: activate
:active? (fn [self] self.state.active)
: on-input
: on-activate
: invoke-interactively
: entry
:widget box
: prompt
: frame
:completions-widget completions
}]
(hbox:pack_start prompt false false 15)
(hbox:pack_start entry true true 5)
(box:pack_start hbox true false 0)
(box:pack_start completions true true 0)
(tset entry :on_changed
(fn [event]
(self:on-input event.text)))
(tset entry :on_activate
(fn [event]
(let [result (self:on-activate event.text)]
(update-widget-state self result))))
self))
{
: activate
: active?
:commander new-commander
: define-command
: on-input
: invoke-interactively
:widget box
:_ {
: reset-state
}
}

View File

@ -19,11 +19,26 @@
, writeText
}:
let pname = "dunlin";
lume = let lua = lua5_3; in lua53Packages.buildLuaPackage rec {
pname = "lume";
version = "1";
src = fetchFromGitHub {
repo = "lume"; owner = "rxi";
rev = "98847e7812cf28d3d64b289b03fad71dc704547d";
hash = "sha256-/u23EqgjjkU8FV9oXvMNXBkY8JAOJUhJAzXTSibJthU=";
};
buildPhase = ":";
installPhase = ''
mkdir -p "$out/share/lua/${lua.luaversion}"
cp lume.lua "$out/share/lua/${lua.luaversion}"
'';
};
lua = lua5_3.withPackages (ps: with ps; [
lgi
luafilesystem
luaposix
readline
lume
]);
fennel_ = lua.pkgs.fennel;
glib_networking_gio = "${glib-networking}/lib/gio/modules";

View File

@ -9,18 +9,12 @@
;;; when we decide how to do an init file/rc file, this will go in it
(local my-keymap {
"g" #(Command.invoke-interactively
"visit-location"
{:buffer "main"})
"M-q" #(Command.invoke-interactively
"quit-browser"
{})
"g" ["visit-location" {:buffer #$1.buffer }]
"M-q" ["quit-browser" {}]
"C-x" {
"C-c"
#(Command.invoke-interactively
"quit-browser"
{})
}
"C-c" ["quit-browser" {}]
"b" ["switch-to-buffer" {}]
}
})
(let [f (Frame.new my-keymap)

View File

@ -1,5 +1,7 @@
(local { : Gtk : Gdk : WebKit2 : cairo } (require :lgi))
(local { : view } (require :fennel))
(local lume (require :lume))
(local Command (require :command))
(local keymap (require :keymap))
@ -8,7 +10,9 @@
(fn new-frame [global-keymap]
(let [hpad 2
vpad 2
self {}
recogniser (keymap.recogniser global-keymap)
commander (Command.commander self)
window (Gtk.Window {
:title "Dunlin"
:default_width 800
@ -30,16 +34,16 @@
(tset window :on_key_release_event
(fn [window event]
(when (not (Command.active?))
(when (not (commander:active?))
(match (recogniser:accept-event event)
c (c)
[name params] (commander:invoke-interactively name params)
(nil prompt) (print "prompted" prompt)))
(when (and event.state.MOD1_MASK
(= event.keyval (string.byte "x")))
(Command.activate))))
(commander:activate))))
(doto container
(: :pack_start Command.widget false false vpad)
(: :pack_start commander.widget false false vpad)
(: :pack_start progress-bar false false vpad)
(: :pack_start contentwidget true true vpad))
(window:add container)
@ -52,11 +56,13 @@
:show-buffer (fn [self b]
(each [_ w (pairs (contentwidget:get_children))]
(w:hide))
(tset self :buffer b)
(contentwidget:pack_start b.webview true true 0)
(b.webview:show))
}]
(table.insert frames f)
f)))
(lume.extend self f)
(table.insert frames self)
self)))
{ :new new-frame :frames frames }

View File

@ -1,6 +1,24 @@
(local { : Gdk } (require :lgi))
(local { : view } (require :fennel))
(local modifier-keyvals
{
;; These aren't canonical or official, this is just the
;; result of pressing keys on my keyboard. If Gtk/Gdk/GI
;; implemented KeyEvent.is_modifier we wouldn't have to
;; do this
65507 :control_l
65505 :shift_l
269025067 :fn
65515 :windows
65513 :alt_l
65027 :alt_gr
65508 :control_r
})
(fn modifier? [keyval]
(. modifier-keyvals keyval))
(fn keychord->spec [keychord]
(let [Mod Gdk.ModifierType
symbol (keychord:match "(%w+)$")
@ -26,13 +44,17 @@
(bor m (. Gdk.ModifierType k)))]
(spec->index {:keyval event.keyval : modmask})))
(fn designates-command? [tbl]
;; a keymap entry has a string as key, a command
;; definition is a numerically-indexed array
(if (. tbl 1) true))
(fn compile-keymap [input]
(collect [k v (pairs input)]
(let [f (-> k keychord->spec spec->index)]
(match (type v)
"function" (values f v)
"table" (values f (compile-keymap v))))))
(if (designates-command? v)
(values f v)
(values f (compile-keymap v))))))
(fn recogniser [source-keymap]
(let [keymap (compile-keymap source-keymap)]
@ -40,20 +62,20 @@
{
:accept-event
(fn [_ e]
(let [c (event->index e)
v (. m c)]
(match (type v)
"table" (do
(when (not (modifier? e.keyval))
(let [c (event->index e)
v (. m c)]
(if v
(if (designates-command? v)
(do
(set m keymap)
v)
(do
(set m v)
(values nil (.. c " ")))
"function" (do
(set m keymap)
v)
"nil" (do
(set m keymap)
(values nil (.. "No binding for " (view e) " ")))
)))
(values nil (.. c " "))))
(do
(set m keymap)
(values nil (.. "No binding for " (view e) " ")))))))
}))

View File

@ -36,14 +36,52 @@ lua's standard types
## next steps
* change define-command so that the parameters are ordered
* [done] change define-command so that the parameters are ordered
* display unbound key error
* ESC to cancel interactive command
* autocomplete command name
* parameters with non-string values (e.g. buffer)
* show current url when command inactive
* show prompts for parameter
* [done] show prompts for parameter
* multiple buffers
- create buffer
- list buffers (where does the output go?)
- find and switch to buffer
how do we do the buffer list thing?
- generate html, or
- use native widgets
native widgets seems neater
- how do we permit commands to insert widgets into the frame?
- how do we get rid of them?
we could have an "output overlay" inserted underneath the commander.
could we use the same thing for completions? we haven't addressed
non-string parameters yet, really
M-x switch-to-buffer
Buffer mai_
+------+ +---------+
| main | | mailing |
+------+ +---------+
-----
so there are two things going on here
1) how to implement switch-to-buffer with appropriate autocomplete
on the buffer name - perhaps involving showing buffer thumbnails etc
2) in emacs, not all buffers are files - e.g. the buffer list, or the
process list, or the magit status buffer - there is a well-used
affordance for elisp to put semi-persistent interactable content
onscreen - do we need such a thing here or is it ok to say "just call
gtk" to command authors
are these the same problem or are they separate problems? do we have
the second problem? What I will do is address the first one and
see if it's generalisable once I've done it.

View File

@ -3,41 +3,48 @@
(local Command (require :command))
(var happened false)
(fn before [] (set happened false) (Command._.reset-state))
(fn before [] (set happened false))
(Command.define-command "no-args-command" #(set happened true) [])
(Command.define-command
"no-args-command"
[]
#(set happened true))
(Command.define-command
"multiply"
(fn [{: a : b }] (set happened (* (tonumber a) (tonumber b))))
[:a #(do "3") :b #(do "2")])
[[:a #{$1 $1} #"3"]
[:b #{$1 $1} #"2"]]
(fn [{: a : b }] (set happened (* (tonumber a) (tonumber b)))))
(before)
(let [(ok err)
(match-try (Command.activate)
{:active true} (Command.on-input "not-a-command")
(let [commander (Command.commander)
(ok err)
(match-try (commander:activate)
{:active true} (commander:on-activate "not-a-command")
(where {:error e :active false} (e:match "can't find command")) true
(catch
x (values nil (view x))))]
(assert ok err))
(before)
(let [(ok err)
(match-try (Command.activate)
{:active true} (Command.on-input "multiply")
{:active true :prompt p1} (Command.on-input "2")
{:active true :prompt p2} (Command.on-input "3")
(let [commander (Command.commander)
(ok err)
(match-try (commander:activate)
{:active true} (commander:on-activate "multiply")
{:active true :prompt p1} (commander:on-activate "2")
{:active true :prompt p2} (commander:on-activate "3")
(where {:active false} (= happened 6)) true
(catch
x (values nil (view x))))]
(assert ok err))
(before)
(let [(ok err)
(let [commander (Command.commander)
(ok err)
(match
(Command.invoke-interactively
(commander:invoke-interactively
"multiply"
{:a "7" :b "9"})
{:a #"7" :b #"9"})
(where {:active false} (= happened 63)) true
x (values nil (.. "wrong answer " (view x) " " (view happened)))
nil (values nil "???"))]

View File

@ -6,13 +6,15 @@
(local Mod Gdk.ModifierType)
(local km
{"a"
{"a" #1
"b" #2}
"b"
{"z" #3}
"c" #4
(local km {
"a" {
"a" ["command-1"]
"b" ["command-2" {:arg-1 "10" :arg-2 "11"}]
}
"b" {
"z" ["command-3"]
}
"c" ["command-4"]
})
(fn fake-key-event [c]
@ -40,8 +42,16 @@
(let [r (keymap.recogniser km)
(ok err)
(match (r:accept-event (fake-key-event "c"))
(where f (= (f) 4)) true
["command-4"] true
x (values false (view x))
nil (values false "???"))]
(assert ok err))
(let [r (keymap.recogniser km)
(ok err)
(match-try
(r:accept-event (fake-key-event "a"))
nil (r:accept-event (fake-key-event "b"))
["command-2" {:arg-1 "10" :arg-2 "11"}] true
(catch x (values false (view x))))]
(assert ok err))