mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-01 00:33:05 +08:00
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:
parent
7373258594
commit
536f2c18dc
347
website/slides/2023-03-london-gophers.md
Normal file
347
website/slides/2023-03-london-gophers.md
Normal 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
14
website/slides/gen.elv
Executable 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
|
141
website/slides/template.html
Normal file
141
website/slides/template.html
Normal 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>
|
Loading…
Reference in New Issue
Block a user