Compare commits

...

7 Commits

Author SHA1 Message Date
Daniel Barlow 9a843e614d explain my conflicted attitude to code contributions 2022-12-23 16:19:42 +00:00
Daniel Barlow db02f34b3e whitespace and etc 2022-12-23 16:19:36 +00:00
Daniel Barlow d840eb8cb0 describe commands and bindings 2022-12-23 16:19:13 +00:00
Daniel Barlow cde0b8cd56 pass keymap into the frame 2022-12-23 15:21:06 +00:00
Daniel Barlow 0befc24bc0 update docs 2022-12-23 15:11:26 +00:00
Daniel Barlow 15c0ce3417 allow modifiers in keymaps 2022-12-23 14:43:29 +00:00
Daniel Barlow ddef8af528 restart tests when entr fails
entr doesn't cope well when emacs makes/removes symlinks that it's
looking at
2022-12-23 14:39:35 +00:00
8 changed files with 214 additions and 48 deletions

View File

@ -1,2 +1,5 @@
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

View File

@ -1,2 +1,2 @@
test: make watch
repl: fennel
test: find . -type d | entr -d sh test/run.sh

View File

@ -47,9 +47,9 @@ in stdenv.mkDerivation rec {
];
nativeBuildInputs = [
lua
pandoc
makeWrapper
copyDesktopItems
pandoc
];
makeFlags = [ "PREFIX=${placeholder "out"}" ];

View File

@ -13,20 +13,108 @@
Dunlin is a GTK-based Webkit browser which can be extended in (indeed,
is mostly written in) [Fennel](https://www.fennel-lang.org).
## Hacking
## Why does the world need another half-assed Webkit browser?
$ nix-shell
nix-shell$ overmind start -D
nix-shell$ overmind connect
It doesn't, really. But me, personally, I think I do. I want
a browser that lets me easily customize it to do things
like:
* watch a local html file and reload when it changes
* find the open tab for my home Fediverse instance (or open one if
needed) and post a message/share a link to what I'm looking at
* search the {Lua reference, Ruby docs, GTK docs} for term instead of
wading through the SEO cesspool that is the internet in 2022
* search in previously "liked" pages for the thing I found three weeks
ago, instead of ... ditto
* find the tab that's playing music and pause the player (instead of
muting it)
* more stuff I haven't thought of
This starts a Fennel interpreter and a test runner in tmux windows. Use `C-b n`
to switch between them or `C-b d` to detach.
Full disclosure: Dunlin today can do none of these things - or much of
anything else either, right now - but if I borrow enough design
decisions from Emacs, I hope some day it will.
$ fennel dunlin.fnl &
$ ./repl.sh
## Installation and getting started
If you're using Nix, you can run `nix-shell` to pull all the
dependencies. Otherwise ... apt-get and Luarocks? Refer to
`default.nix` and `shell.nix`, and obtain:
* All Lua packages listed in any `lua5_3.withPackages` stanza
* All system packages listed in any `buildInputs` or `nativeBuildInputs`
stanzas.
To run the browser,
```sh
$ fennel dunlin.fnl
```
Dunlin will open a socket in `$XDG_RUNTIME_DIR` to allow communication
with a Fennel REPL. The repl.sh script uses
[socat](http://www.dest-unreach.org/socat/) to connect to it.
with a Fennel REPL. You can run the `repl.sh` script to connect to it
with [socat](http://www.dest-unreach.org/socat/).
### Running tests
Tests are in `test/*.fnl`. The test coverage is not 100% nor will it
ever be, probably: tests exist only for the bits that were hard to
write (algorithmically complex) and easy to test (not full of UI).
`make watch` will watch the filesystem in this directory and run the
tests whenever something changes.
### Running everything
There's a `Procfile` that starts the test runner and a Fennel repl and
probably in time some other useful things. I use it with [Overmind](https://github.com/DarthSim/overmind#readme) - `overmind start -D; overmind connect`
to create a tmux session.
## Customizing
_This is all quite fluid right now and I reserve the right to change
things_. In particular, I anticipate the need to bind to non-key events
(mouse events, on-screen buttons, touch gestures)
(Among) the concepts you need to know here are "commands" and "bindings".
* A "command" is a chunk of code that may be invoked interactively,
plus a descrition of the parameters it needs and their default values.
For example, `visit-location` needs a `url` parameter for the location
and a `buffer` parameter that says which tab should visit it.
* A "binding" is a sequence of keystrokes which map to a command: it
may supply zero or more parameter values to the command. For example,
(as of git commit cde0b8cd56d; YMMV if you're reading this significantly
before or after 2022-12-23) the `g` binding invokes `visit-location`
with the `buffer` parameter set to `main`, and the `url` parameter
unset.
When a command is invoked without all parameter values, Dunlin will
prompt for each missing parameter in the "commander" text entry widget.
A command with no binding may be invoked by pressing `M-x` (hold down
the "meta" or "alt" key and press `x`) and then typing the command name.
To see how commands are implemented, read the code in `command.fnl`.
There is a simple keymap in `dunlin.fnl`, and you can see the details
of how keymaps work in `keymap.fnl`
## Contributing
If you like (or see the potential in) Dunlin enough to want to
contribute to it, that is awesome and you have just made my day!
However, I need to point out that it's under active development:
my vision for how it'll end up is indefinite in some places and
poorly articulated in others. So, please, don't send me code
that you're personally invested in and would be disappointed if
I reject/ignore it because it doesn't fit some plan that you
didn't know about when you wrote it.
You can checkout Dunlin from https://gti.telent.net/dan/dunlin. I
haven't put it on Github because I have uneasy feelings about the
ongoing centralisation of Open Source, but certainly don't let that
stop you. You can [email me your change](mailto:dan@telent.net) as a
patch, or you can point me at code on your preferred source code
hosting service - whatever works for you. I reserve the right to be
less flexible in future if I start drowning in changes.

View File

@ -2,10 +2,28 @@
(local lfs (require :lfs))
(local { : Gtk : Gdk : WebKit2 : cairo } lgi)
(local Command (require :command))
(local Frame (require :frame))
(local Buffer (require :buffer))
(let [f (Frame.new)
;;; 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"
{})
"C-x" {
"C-c"
#(Command.invoke-interactively
"quit-browser"
{})
}
})
(let [f (Frame.new my-keymap)
b (Buffer.new "main")]
(f:show-buffer b)
(b:visit (.. "file://" (lfs.currentdir) "/doc/index.html")))

View File

@ -5,25 +5,10 @@
(var frames [])
(fn new-frame []
(fn new-frame [global-keymap]
(let [hpad 2
vpad 2
recogniser (keymap.recogniser
{
"g" #(Command.invoke-interactively
"visit-location"
{:buffer "main"})
"q" #(Command.invoke-interactively
"quit-browser"
{})
"c" {
"x"
#(Command.invoke-interactively
"quit-browser"
{})
}
})
recogniser (keymap.recogniser global-keymap)
window (Gtk.Window {
:title "Dunlin"
:default_width 800
@ -54,7 +39,7 @@
(tset window :on_key_release_event
(fn [window event]
(when (not (Command.active?))
(match (recogniser:accept (string.char event.keyval))
(match (recogniser:accept-event event)
c (c)
(nil prompt) (print "prompted" prompt)))
(when (and event.state.MOD1_MASK

View File

@ -1,19 +1,64 @@
(local { : Gdk } (require :lgi))
(local { : view } (require :fennel))
(fn keychord->spec [keychord]
(let [Mod Gdk.ModifierType
symbol (keychord:match "(%w+)$")
upper? (and (symbol:match "%u") true)
modmask (accumulate [m (if upper? Mod.SHIFT_MASK 0)
v (keychord:gmatch "(%w+)-")]
(match (v:lower)
"m" (bor m Mod.MOD1_MASK)
"c" (bor m Mod.CONTROL_MASK)
"s" (bor m Mod.MOD4_MASK)))]
{
:keyval (string.byte (symbol:lower))
: modmask
}))
(fn spec->index [spec]
(string.format "%d:%d" spec.keyval spec.modmask))
(fn event->index [event]
(let [modmask
(accumulate [m 0
k _ (pairs event.state)]
(bor m (. Gdk.ModifierType k)))]
(spec->index {:keyval event.keyval : modmask})))
(fn recogniser [keymap]
(var m keymap)
{
:accept
(fn [_ c]
(let [v (. m c)]
(match (type v)
"table" (do
(set m v)
(values nil (.. c " ")))
"function" (do
(set m keymap)
v))))
})
(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))))))
(fn recogniser [source-keymap]
(let [keymap (compile-keymap source-keymap)]
(var m keymap)
{
:accept-event
(fn [_ e]
(let [c (event->index e)
v (. m c)]
(match (type v)
"table" (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) " ")))
)))
}))
{ : recogniser }
{ : recogniser
:_ {
;; symbols in _ are exported only for testing
: keychord->spec
} }

View File

@ -1,7 +1,11 @@
(local { : view } (require :fennel))
(local { : Gdk } (require :lgi))
(local keymap (require :keymap))
(local Mod Gdk.ModifierType)
(local km
{"a"
{"a" #1
@ -11,10 +15,33 @@
"c" #4
})
(fn fake-key-event [c]
{:keyval (string.byte c)
:state {}
})
(let [s (keymap._.keychord->spec "q")]
(match s
{:keyval 113 :modmask 0} true
_ (assert false (view s))))
(let [s (keymap._.keychord->spec "C-a")]
(match s
{:keyval 97 :modmask 4} true
_ (assert false (view s))))
(let [s (keymap._.keychord->spec "C-M-Z")]
(match s
{:keyval 122 :modmask 13} true
_ (assert false (view s))))
(let [r (keymap.recogniser km)
(ok err)
(match (r:accept "c")
(match (r:accept-event (fake-key-event "c"))
(where f (= (f) 4)) true
x (values false (x)))]
x (values false (view x))
nil (values false "???"))]
(assert ok err))