website/slides: Slidedeck sources and tools

- Slidedecks are Markdown files; conversion uses the md2html tool of the website
  toolchain

- Individual slides are delimited by <hr> (*** in Markdown)

- Splitting slides, switching slides and hash synchronization happen in
  JavaScript
This commit is contained in:
Qi Xiao 2024-06-28 20:20:18 +08:00
parent 7373258594
commit 536f2c18dc
3 changed files with 502 additions and 0 deletions

View File

@ -0,0 +1,347 @@
# Elvish
An expressive, versatile cross-platform shell implemented in Go
Qi Xiao (xiaq)
2023-03-22 @ London Gophers
***
# Intro
- Software engineer at Google
- I don't speak for my employer, etc. etc.
- Started Elvish in 2013
***
# Why build a shell
- I use a shell every day I use a computer
- Traditional shells aren't good enough
- Want a shell with
- Nice interactive features
- Serious programming constructs
***
# Why not build a shell
- Too hard for users to switch
- Hopefully overcomeable
- "Shells are *supposed* to be primitive and arcane"
- Defeatism
***
# Part 1:
# Elvish as a shell and programming language
***
# A better shell
- Enhanced interactive features
- Location mode (Ctrl-L)
- Navigation mode (Ctrl-N)
- This works like a traditional shell (only new feature is `**`):
```elvish
cat **.go | grep -v '^$' | wc -l
```
***
# Somewhat new takes
- Get command output with `()`:
```elvish
wget dl.elv.sh/(go env GOGOS)-(go env GOARCH)/elvish-HEAD
```
- Variables must be declared:
```elvish
var lorem = foo
echo $lorme # Error!
```
- Variable values are never split:
```elvish
var x = 'foo bar'
touch $x
```
***
# Data structures
- Lists: `[foo bar]`
- Maps: `[&foo=bar]`
- Index them:
```elvish
var l = [foo bar]; echo $l[0]
var m = [&foo=bar]; echo $m[foo]
```
- Nest them:
```elvish
var l = [[foo] [&[foo]=[bar]]]
echo $l[1][[foo]]
```
***
# Data structures in "shell use cases"
- Automation with multiple parameters:
```elvish
for host [[&name=foo &port=22] [&name=bar &port=2200]] {
scp -P $host[port] elvish root@$host[name]:/usr/local/bin/
}
```
- Abbreviations are maps:
```elvish
set edit:abbr[xx] = '> /dev/null'
```
- Better programming language leads to better shell
***
# Lambdas!
- Lambdas:
```elvish
var f = {|x| echo 'Hello '$x }
$f world
var g = { echo 'Hello' } # Omit empty arg list
$g
```
***
# Lambdas in "shell use cases"
- Prompts are lambdas:
- ```elvish
set edit:prompt = { put (whoami)@(tilde-abbr $pwd)'> ' }
```
- No mini-language like `PS1='\u@\w> '`
- Configure command completion with a map to lambdas:
```elvish
set edit:completion:arg-completer[foo] = {|@x|
echo lorem
echo ipsum
}
```
***
# Outputting and piping values
- Arithmetic:
```elvish
* 7 (+ 1 5)
```
- String processing:
```elvish
use str
for x [*.jpg] {
gm convert $x (str:trim-suffix $x .jpg).png
}
```
- Stream processing:
```elvish
put [x y] [x] | count
put [x y] [x] | each {|v| put $v[0] }
```
***
# Conclusions of part 1
- Familiar shell
- With a real programming language
- Better programming language -> better shell
- Many features not covered
- Environment variables, exceptions, user-defined modules, ...
***
# Part 2:
# Elvish as a Go project
- Implementation
- Experience of using Go
- CI/CD practice
***
# Implementation overview
- Frontend (parser)
- Hand written parser
- Interface and recursion
- `type Node interface { parse(*parser) }`
- Backend (interpreter)
- Compile the parse tree into an op tree
- Interface and recursion
- `type effectOp interface { exec(*Frame) Exception }`
- Terminal UI
- Arcane escape codes
***
# Notable Go features in Elvish's runtime
- Running external commands: `os.StartProcess`
- Pipelines: goroutines, `sync.WaitGroup`
- Outputting and piping values: `chan any`
- Big numbers: `math/big`
- Go standard library
- Elvish's `str:trim-suffix` is just Go's `strings.TrimSuffix`, etc.
- Reflection-based binding
***
# Why Go?
- Reasonably performant
- Suitable runtime (goroutines, GC)
- Fast compilation and easy cross-compilation
- Rust wasn't released yet
***
# Wishlist
- Nil safety
- Plugin support on more platforms
- Faster reflection
***
# Experience over the years
- Go 1.1 (!) was the latest version when I started Elvish
- Relatively few changes over the years
- 1.5: vendoring
- 1.11: modules
- 1.13: `-trimpath`
- 1.16: `//go:embed`
- 1.18: Generics, fuzzing
***
# CI
- GitHub Actions
- Tests, go vet, staticcheck, etc.
- Uploading test coverages to codecov.io
- Cirrus CI for more platforms
- {Free Net Open}BSD
- Linux ARM64
- Both are free for Elvish's current use cases
***
# Website and prebuilt binaries
- <https://elv.sh/>
- Webhook
- Building the website
- A custom CommonMark implementation
- A custom static site generator
- Building the binaries
- Reproducible
- Verified by both CI environments
- Two nodes globally, with geo DNS
- ~ $20 per month (VPS + domain name + DNS)
***
# Learn more
<https://elv.sh>

14
website/slides/gen.elv Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env elvish
use str
use flag
fn main { |&title=Presentation md html|
var content = (go run src.elv.sh/website/cmd/md2html < $md | slurp)
slurp < template.html |
str:replace '$common-css' (cat ../reset.css ../sgr.css | slurp) (one) |
str:replace '$title' $title (one) |
str:replace '$content' $content (one) |
print (one) > $html
}
flag:call $main~ $args

View File

@ -0,0 +1,141 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>$title</title>
<style>
$common-css
h1 { font-size: 2em; margin: 1em 0; }
li { margin: 0.2em 0 0.2em 1em; }
p { margin: 0.2em 0; }
:root {
font-size: min(4vh, 2.5vw);
}
html, body {
height: 100%;
}
body {
line-height: 1.2;
}
/* Fallback style with no JS */
#progress {
display: none;
}
#raw-content {
padding: 0.5rem 2rem 0;
}
/* Style with JS */
.has-js #raw-content, .has-js section {
display: none;
}
.has-js #progress {
display: block;
position: absolute;
bottom: 0.5rem;
width: 100%;
text-align: center;
font-size: 0.5rem;
color: gray;
}
.has-js section.current {
display: block;
padding: 0.5rem 2rem 0;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.body.classList.add('has-js');
const slides = createSlides();
let current = 0;
switchToHash();
document.body.addEventListener('keydown', (event) => {
if (event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) {
return;
}
if (['ArrowLeft', 'ArrowUp', 'k', 'h'].includes(event.key)) {
switchToPrev();
event.preventDefault();
} else if (['ArrowRight', 'ArrowDown', 'j', 'l', ' '].includes(event.key)) {
switchToNext();
event.preventDefault();
}
});
document.body.addEventListener('click', (event) => {
const width = event.target.getBoundingClientRect().width;
if (event.clientX < width / 3) {
switchToPrev();
} else {
switchToNext();
}
}, true);
function createSlides() {
// Use <hr> tags to split the raw content into slides (which are
// <section> elements), and use the first element that has an id as the
// slide's id.
const slides = [];
let slide = {element: document.createElement('section'), id: undefined};
for (const child of [...document.getElementById('raw-content').childNodes]) {
if (child.tagName === 'HR') {
// If there's no element with an id, use the index as a fallback.
slide.id ||= String(slides.length)
slides.push(slide);
slide = {element: document.createElement('section'), id: undefined};
continue;
}
slide.element.appendChild(child);
if (!slide.id && child.id) {
slide.id = child.id
}
}
if (slide.element.childNodes.length > 0) {
slide.id ||= String(slides.length);
slides.push(slide)
}
for (const slide of slides) {
document.body.appendChild(slide.element);
}
return slides;
}
function switchToHash() {
const id = decodeURIComponent(document.location.hash.substring(1));
const i = slides.findIndex((slide) => slide.id === id);
if (i !== -1) {
switchTo(i);
} else {
switchTo(0);
}
}
function switchToPrev() { switchTo(Math.max(0, current - 1)); }
function switchToNext() { switchTo(Math.min(slides.length - 1, current + 1)); }
function switchTo(i) {
slides[current].element.className = '';
slides[i].element.className = 'current';
current = i;
if (i === 0) {
// Remove hash entirely
history.pushState(null, null, ' ');
} else {
document.location.hash = slides[i].id;
}
document.getElementById('progress').innerText = (i + 1) + ' / ' + slides.length;
}
});
</script>
</head>
<body>
<div id="progress">? / ?</div>
<div id="raw-content">
$content
</div>
</body>
</html>