Compare commits
7 Commits
d1e3b8c658
...
9a843e614d
Author | SHA1 | Date | |
---|---|---|---|
9a843e614d | |||
db02f34b3e | |||
d840eb8cb0 | |||
cde0b8cd56 | |||
0befc24bc0 | |||
15c0ce3417 | |||
ddef8af528 |
3
Makefile
3
Makefile
@ -1,2 +1,5 @@
|
|||||||
doc/index.html: doc/index.md
|
doc/index.html: doc/index.md
|
||||||
pandoc -t html -f gfm < $< > $@
|
pandoc -t html -f gfm < $< > $@
|
||||||
|
|
||||||
|
watch:
|
||||||
|
while true ; do ( find . -type f | entr -d sh test/run.sh ) ;done
|
||||||
|
2
Procfile
2
Procfile
@ -1,2 +1,2 @@
|
|||||||
|
test: make watch
|
||||||
repl: fennel
|
repl: fennel
|
||||||
test: find . -type d | entr -d sh test/run.sh
|
|
@ -47,9 +47,9 @@ in stdenv.mkDerivation rec {
|
|||||||
];
|
];
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
lua
|
lua
|
||||||
|
pandoc
|
||||||
makeWrapper
|
makeWrapper
|
||||||
copyDesktopItems
|
copyDesktopItems
|
||||||
pandoc
|
|
||||||
];
|
];
|
||||||
|
|
||||||
makeFlags = [ "PREFIX=${placeholder "out"}" ];
|
makeFlags = [ "PREFIX=${placeholder "out"}" ];
|
||||||
|
108
doc/index.md
108
doc/index.md
@ -13,20 +13,108 @@
|
|||||||
Dunlin is a GTK-based Webkit browser which can be extended in (indeed,
|
Dunlin is a GTK-based Webkit browser which can be extended in (indeed,
|
||||||
is mostly written in) [Fennel](https://www.fennel-lang.org).
|
is mostly written in) [Fennel](https://www.fennel-lang.org).
|
||||||
|
|
||||||
## Hacking
|
## Why does the world need another half-assed Webkit browser?
|
||||||
|
|
||||||
$ nix-shell
|
It doesn't, really. But me, personally, I think I do. I want
|
||||||
nix-shell$ overmind start -D
|
a browser that lets me easily customize it to do things
|
||||||
nix-shell$ overmind connect
|
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`
|
Full disclosure: Dunlin today can do none of these things - or much of
|
||||||
to switch between them or `C-b d` to detach.
|
anything else either, right now - but if I borrow enough design
|
||||||
|
decisions from Emacs, I hope some day it will.
|
||||||
|
|
||||||
$ fennel dunlin.fnl &
|
## Installation and getting started
|
||||||
$ ./repl.sh
|
|
||||||
|
|
||||||
|
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
|
Dunlin will open a socket in `$XDG_RUNTIME_DIR` to allow communication
|
||||||
with a Fennel REPL. The repl.sh script uses
|
with a Fennel REPL. You can run the `repl.sh` script to connect to it
|
||||||
[socat](http://www.dest-unreach.org/socat/) 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.
|
||||||
|
20
dunlin.fnl
20
dunlin.fnl
@ -2,10 +2,28 @@
|
|||||||
(local lfs (require :lfs))
|
(local lfs (require :lfs))
|
||||||
(local { : Gtk : Gdk : WebKit2 : cairo } lgi)
|
(local { : Gtk : Gdk : WebKit2 : cairo } lgi)
|
||||||
|
|
||||||
|
(local Command (require :command))
|
||||||
(local Frame (require :frame))
|
(local Frame (require :frame))
|
||||||
(local Buffer (require :buffer))
|
(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")]
|
b (Buffer.new "main")]
|
||||||
(f:show-buffer b)
|
(f:show-buffer b)
|
||||||
(b:visit (.. "file://" (lfs.currentdir) "/doc/index.html")))
|
(b:visit (.. "file://" (lfs.currentdir) "/doc/index.html")))
|
||||||
|
21
frame.fnl
21
frame.fnl
@ -5,25 +5,10 @@
|
|||||||
|
|
||||||
(var frames [])
|
(var frames [])
|
||||||
|
|
||||||
(fn new-frame []
|
(fn new-frame [global-keymap]
|
||||||
(let [hpad 2
|
(let [hpad 2
|
||||||
vpad 2
|
vpad 2
|
||||||
recogniser (keymap.recogniser
|
recogniser (keymap.recogniser global-keymap)
|
||||||
{
|
|
||||||
"g" #(Command.invoke-interactively
|
|
||||||
"visit-location"
|
|
||||||
{:buffer "main"})
|
|
||||||
"q" #(Command.invoke-interactively
|
|
||||||
"quit-browser"
|
|
||||||
{})
|
|
||||||
"c" {
|
|
||||||
"x"
|
|
||||||
#(Command.invoke-interactively
|
|
||||||
"quit-browser"
|
|
||||||
{})
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
window (Gtk.Window {
|
window (Gtk.Window {
|
||||||
:title "Dunlin"
|
:title "Dunlin"
|
||||||
:default_width 800
|
:default_width 800
|
||||||
@ -54,7 +39,7 @@
|
|||||||
(tset window :on_key_release_event
|
(tset window :on_key_release_event
|
||||||
(fn [window event]
|
(fn [window event]
|
||||||
(when (not (Command.active?))
|
(when (not (Command.active?))
|
||||||
(match (recogniser:accept (string.char event.keyval))
|
(match (recogniser:accept-event event)
|
||||||
c (c)
|
c (c)
|
||||||
(nil prompt) (print "prompted" prompt)))
|
(nil prompt) (print "prompted" prompt)))
|
||||||
(when (and event.state.MOD1_MASK
|
(when (and event.state.MOD1_MASK
|
||||||
|
75
keymap.fnl
75
keymap.fnl
@ -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]
|
(fn compile-keymap [input]
|
||||||
(var m keymap)
|
(collect [k v (pairs input)]
|
||||||
{
|
(let [f (-> k keychord->spec spec->index)]
|
||||||
:accept
|
(match (type v)
|
||||||
(fn [_ c]
|
"function" (values f v)
|
||||||
(let [v (. m c)]
|
"table" (values f (compile-keymap v))))))
|
||||||
(match (type v)
|
|
||||||
"table" (do
|
(fn recogniser [source-keymap]
|
||||||
(set m v)
|
(let [keymap (compile-keymap source-keymap)]
|
||||||
(values nil (.. c " ")))
|
(var m keymap)
|
||||||
"function" (do
|
{
|
||||||
(set m keymap)
|
:accept-event
|
||||||
v))))
|
(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
|
||||||
|
} }
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
(local { : view } (require :fennel))
|
(local { : view } (require :fennel))
|
||||||
|
(local { : Gdk } (require :lgi))
|
||||||
|
|
||||||
(local keymap (require :keymap))
|
(local keymap (require :keymap))
|
||||||
|
|
||||||
|
|
||||||
|
(local Mod Gdk.ModifierType)
|
||||||
|
|
||||||
(local km
|
(local km
|
||||||
{"a"
|
{"a"
|
||||||
{"a" #1
|
{"a" #1
|
||||||
@ -11,10 +15,33 @@
|
|||||||
"c" #4
|
"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)
|
(let [r (keymap.recogniser km)
|
||||||
(ok err)
|
(ok err)
|
||||||
(match (r:accept "c")
|
(match (r:accept-event (fake-key-event "c"))
|
||||||
(where f (= (f) 4)) true
|
(where f (= (f) 4)) true
|
||||||
x (values false (x)))]
|
x (values false (view x))
|
||||||
|
nil (values false "???"))]
|
||||||
|
|
||||||
(assert ok err))
|
(assert ok err))
|
||||||
|
Loading…
Reference in New Issue
Block a user