Full Code of maaslalani/slides for AI

main c6eea3330053 cached
53 files
81.8 KB
28.4k tokens
109 symbols
1 requests
Download .txt
Repository: maaslalani/slides
Branch: main
Commit: c6eea3330053
Files: 53
Total size: 81.8 KB

Directory structure:
gitextract_svh9gyz7/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── dependabot.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── goreleaser.yml
│       └── test.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── SECURITY.md
├── docs/
│   └── development/
│       └── README.md
├── examples/
│   ├── ascii_slides.md
│   ├── code_blocks.md
│   ├── custom_remote_theme.md
│   ├── custom_theme.md
│   ├── import.md
│   ├── metadata.md
│   ├── preprocess.md
│   ├── slides.md
│   └── theme.json
├── go.mod
├── go.sum
├── internal/
│   ├── cmd/
│   │   └── serve.go
│   ├── code/
│   │   ├── code.go
│   │   ├── code_test.go
│   │   ├── comments.go
│   │   ├── comments_test.go
│   │   ├── execute_test.go
│   │   └── languages.go
│   ├── file/
│   │   ├── file.go
│   │   └── file_test.go
│   ├── meta/
│   │   ├── meta.go
│   │   └── meta_test.go
│   ├── model/
│   │   ├── model.go
│   │   └── tutorial.md
│   ├── navigation/
│   │   ├── navigation.go
│   │   ├── navigation_test.go
│   │   ├── search.go
│   │   └── search_test.go
│   ├── process/
│   │   ├── execute_test.go
│   │   ├── process.go
│   │   └── process_test.go
│   └── server/
│       ├── middleware.go
│       └── server.go
├── main.go
├── snap/
│   └── snapcraft.yaml
└── styles/
    ├── styles.go
    ├── styles_test.go
    └── theme.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
github: maaslalani


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

**Smartphone (please complete the following information):**
 - Device: [e.g. iPhone6]
 - OS: [e.g. iOS8.1]
 - Browser [e.g. stock browser, safari]
 - Version [e.g. 22]

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: "gomod"
    directory: "/"
    schedule:
      interval: "daily"


================================================
FILE: .github/pull_request_template.md
================================================
Fixes #...

### Changes Introduced
- 
- 
-


================================================
FILE: .github/workflows/goreleaser.yml
================================================
name: goreleaser

on:
  push:
    tags:
      - '*'

jobs:
  goreleaser:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Go
        uses: actions/setup-go@v2
        with:
          go-version: 1.17
      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v2
        with:
          distribution: goreleaser
          version: latest
          args: release --rm-dist
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/test.yml
================================================
name: test

on: [ push, pull_request ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Go
        uses: actions/setup-go@v2
        with:
          go-version: 1.17

      - name: Lint
        uses: golangci/golangci-lint-action@v2

      - name: Test
        run: go test -race -v -short ./...


================================================
FILE: .gitignore
================================================
/slides
.idea
slides_ed25519
slides_ed25519.pub


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Code of Conduct

Be nice please!


================================================
FILE: CONTRIBUTING.md
================================================
Take a look at the [Development Docs](./docs/development/README.md).

Pull requests are welcome!


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 Maas Lalani

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
make:
	go run main.go examples/slides.md

test:
	go test ./... -short

build:
	go build -o slides


================================================
FILE: README.md
================================================
# Slides

Slides in your terminal.

<p align="center">
  <img src="./assets/slides-1.gif?raw=true" alt="Slides Presentation" />
</p>

### Installation
[![Homebrew](https://img.shields.io/badge/dynamic/json.svg?url=https://formulae.brew.sh/api/formula/slides.json&query=$.versions.stable&label=homebrew)](https://formulae.brew.sh/formula/slides)
[![Snapcraft](https://snapcraft.io/slides/badge.svg)](https://snapcraft.io/slides)
[![AUR](https://img.shields.io/aur/version/slides?label=AUR)](https://aur.archlinux.org/packages/slides)

<details markdown="block">
<summary>Instructions</summary>

#### MacOS
```
brew install slides
```
#### Arch
```
yay -S slides
```
#### Nixpkgs (unstable)
```
nix-env -iA nixpkgs.slides
```
#### Any Linux Distro running `snapd`
```
sudo snap install slides
```
#### Go
```
go install github.com/maaslalani/slides@latest
```
From source:
```
git clone https://github.com/maaslalani/slides.git
cd slides
go install
```

You can also download a binary from the [releases](https://github.com/maaslalani/slides/releases) page.

</details>


### Usage
Create a simple markdown file that contains your slides:

````markdown
# Welcome to Slides
A terminal based presentation tool

---

## Everything is markdown
In fact, this entire presentation is a markdown file.

---

## Everything happens in your terminal
Create slides and present them without ever leaving your terminal.

---

## Code execution
```go
package main

import "fmt"

func main() {
  fmt.Println("Execute code directly inside the slides")
}
```

You can execute code inside your slides by pressing `<C-e>`,
the output of your command will be displayed at the end of the current slide.

---

## Pre-process slides

You can add a code block with three tildes (`~`) and write a command to run *before* displaying
the slides, the text inside the code block will be passed as `stdin` to the command
and the code block will be replaced with the `stdout` of the command.

```
~~~graph-easy --as=boxart
[ A ] - to -> [ B ]
~~~
```

The above will be pre-processed to look like:

┌───┐  to   ┌───┐
│ A │ ────> │ B │
└───┘       └───┘

For security reasons, you must pass a file that has execution permissions
for the slides to be pre-processed. You can use `chmod` to add these permissions.

```bash
chmod +x file.md
```

````

Checkout the [example slides](https://github.com/maaslalani/slides/tree/main/examples).

Then, to present, run:
```
slides presentation.md
```

If given a file name, `slides` will automatically look for changes in the file and update the presentation live.

`slides` also accepts input through `stdin`:
```
curl http://example.com/slides.md | slides
```

Go to the first slide with the following key sequence:
* <kbd>g</kbd> <kbd>g</kbd>

Go to the next slide with any of the following key sequences:
* <kbd>space</kbd>
* <kbd>right</kbd>
* <kbd>down</kbd>
* <kbd>enter</kbd>
* <kbd>n</kbd>
* <kbd>j</kbd>
* <kbd>l</kbd>
* <kbd>Page Down</kbd>
* number + any of the above (go forward n slides)

Go to the previous slide with any of the following key sequences:
* <kbd>left</kbd>
* <kbd>up</kbd>
* <kbd>p</kbd>
* <kbd>h</kbd>
* <kbd>k</kbd>
* <kbd>N</kbd>
* <kbd>Page Up</kbd>
* number + any of the above (go back n slides)

Go to a specific slide with the following key sequence:

* number + <kbd>G</kbd>

Go to the last slide with the following key:

* <kbd>G</kbd>

### Search

To quickly jump to the right slide, you can use the search function.

Press <kbd>/</kbd>, enter your search term and press <kbd>Enter</kbd>  
(*The search term is interpreted as a regular expression. The `/i` flag causes case-insensitivity.*).

Press <kbd>ctrl+n</kbd> after a search to go to the next search result.

### Code Execution

If slides finds a code block on the current slides it can execute the code block and display the result as virtual text
on the screen.

Press <kbd>ctrl+e</kbd> on a slide with a code block to execute it and display the result.

### Pre-processing

You can add a code block with three tildes (`~`) and write a command to run
*before* displaying the slides, the text inside the code block will be passed
as `stdin` to the command and the code block will be replaced with the `stdout`
of the command. Wrap the pre-processed block in three backticks to keep
proper formatting and new lines.

````
```
~~~graph-easy --as=boxart
[ A ] - to -> [ B ]
~~~
```
````

The above will be pre-processed to look like:

```
┌───┐  to   ┌───┐
│ A │ ────> │ B │
└───┘       └───┘
```

For security reasons, you must pass a file that has execution permissions
for the slides to be pre-processed. You can use `chmod` to add these permissions.

```bash
chmod +x file.md
```

### Configuration

`slides` allows you to customize your presentation's look and feel with metadata at the top of your `slides.md`.

> This section is entirely optional, `slides` will use sensible defaults if this section or any field in the section is omitted.

```yaml
---
theme: ./path/to/theme.json
author: Gopher
date: MMMM dd, YYYY
paging: Slide %d / %d
---
```

* `theme`: Path to `json` file containing a [glamour
  theme](https://github.com/charmbracelet/glamour/tree/master/styles), can also
  be a link to a remote `json` file which slides will fetch before presenting.
* `author`: A `string` to display on the bottom-left corner of the presentation
  view. Defaults to the OS current user's full name. Can be empty to hide the author.
* `date`: A `string` that is used to format today's date in the `YYYY-MM-DD` format. If the date is not a valid
  format, the string will be displayed. Defaults to `YYYY-MM-DD`.
* `paging`: A `string` that contains 0 or more `%d` directives. The first `%d`
  will be replaced with the current slide number and the second `%d` will be
  replaced with the total slides count. Defaults to `Slide %d / %d`.
  You will need to surround the paging value with quotes if it starts with `%`.

#### Date format

Given the date _January 02, 2006_:

| Value  | Translates to |
|--------|---------------|
| `YYYY` | 2006          |
| `YY`   | 06            |
| `MMMM` | January       |
| `MMM`  | Jan           |
| `MM`   | 01            |
| `mm`   | 1             |
| `DD`   | 02            |
| `dd`   | 2             |

### SSH

Slides is accessible over `ssh` if hosted on a machine through the `slides
serve [file]` command.

On a machine, run:

```
slides serve [file]
```

Then, on another machine (or same machine), `ssh` into the port specified by
the `slides serve [file]` command:
```
ssh 127.0.0.1 -p 53531
```

You will be able to access the presentation hosted over SSH! You can use this
to present with `slides` from a computer that doesn't have `slides` installed,
but does have `ssh`. Or, let your viewers have access to the slides on their
own computer without needing to download `slides` and the presentation file.

### Alternatives

**Credits**: This project was heavily inspired by [`lookatme`](https://github.com/d0c-s4vage/lookatme).

* [`lookatme`](https://github.com/d0c-s4vage/lookatme)
* [`sli.dev`](https://sli.dev/)
* [`sent`](https://tools.suckless.org/sent/)
* [`presenterm`](https://github.com/mfontanini/presenterm)

### Development
See the [development documentation](./docs/development)


================================================
FILE: SECURITY.md
================================================
# Security Policy

## Reporting a Vulnerability

Email [maas@lalani.dev](mailto:maas@lalani.dev)


================================================
FILE: docs/development/README.md
================================================
# Development

Make changes, and test them by running:
```
make
```

This will run `go run main.go examples/slides.md`, you can then ensure
everything still works.

If you're adding a feature that requires a specific piece of markdown, you can
add a file with your test case into `examples/<test>.md` and iterate on that file.

Ensure tests are still passing
```
make test
```

### Breaking Changes
Most changes should be entirely backwards compatible.
Ensure that `slides examples/slides.md` still works.

### Codebase
Initialization (command-line interface, defaults) happens in [`cmd/root.go`](../../cmd/root.go).
Interaction (controls, input, output) happens in [`model.go`](../../internal/model/model.go)
Optional configuration (e.g. `theme: dark`) can be added to [`meta.go`](../../internal/meta/meta.go)


================================================
FILE: examples/ascii_slides.md
================================================
---
theme: ascii
---

# Welcome to Slides
A terminal based presentation tool

```go
package main

import "fmt"

func main() {
  fmt.Println("Written in Go!")
}
```

---

## Everything is markdown
In fact this entire presentation is a markdown file

---

# h1
## h2
### h3
#### h4
##### h5
###### h6

# Markdown components
You can use everything in markdown!
* Like bulleted list
* You know the deal

1. Numbered lists too

| Tables | Too    |
| ------ | ------ |
| Even   | Tables |

---

All you need to do is separate slides with triple dashes `---` on a separate line,
like so:

```markdown
# Slide 1
Some stuff

--- 

# Slide 2
Some other stuff
```


================================================
FILE: examples/code_blocks.md
================================================
# Code blocks

Slides allows you to execute code blocks directly inside your slides!

Just press `ctrl+e` and the result of the code block will be displayed as virtual text in your slides.

Currently supported languages:

<!-- Use comments in your markdown! -->

* `bash`
* `zsh`
* `fish`
* `elixir`
* `go`
* `javascript`
* `python`
* `ruby`
* `perl`
* `rust`
* `java`
* `cpp`
* `swift`
* `dart`
* `v`
<!-- * `secret` -->

---

### Bash

```bash
ls
```

---

### Zsh

```zsh
ls
```

---

### Fish

```fish
ls
```

---

### Elixir

```elixir
IO.puts "Hello, world!"
```

---

### Go

Use `///` to hide verbose code but still allow the ability to execute it.

If you press `y` to copy (yank) this code block it will return the full snippet.

And, if you press `ctrl+e` it will run the program without error, even though
what is being displayed is not a valid go program because we have commented out
some boilerplate to focus on the important parts.

```go
///package main
///
import "fmt"
///
///func main() {
fmt.Println("Hello, world!")
///}
```

---

### Javascript

```javascript
console.log("Hello, world!")
```

---

### Lua

```lua
print("Hello, World!")
```

---

### Python

```python
print("Hello, world!")
```

---

### Ruby

```ruby
puts "Hello, world!"
```

---

### Perl

```perl
print ("hello, world");
```

---

### Rust

```rust
fn main() {
    println!("Hello, world!");
}
```

---

### Java
```java
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}
```

---

### Julia
```julia
println("Hello, world!")
```

---

### C++
```cpp
#include <iostream>

int main() {
    std::cout << "Hello, world!" << std::endl;
    return 0;
}
```

---

### Swift
```swift
print("Hello, world!")
```

---

### Dart
```dart
void main() {
  print("Hello, world!");
}
```

### V

```v
println('Hello, world!')
```

---

### Scala

```scala
//> using dep com.lihaoyi::pprint:0.8.1

object Main extends App {
  println("Hello")
}
```


================================================
FILE: examples/custom_remote_theme.md
================================================
---
theme: https://github.com/maaslalani/slides/raw/main/styles/theme.json
---

# Slides

The theme of this slide is fetched from https://github.com/maaslalani/slides/raw/main/styles/theme.json, the title above should be green.


================================================
FILE: examples/custom_theme.md
================================================
---
theme: ./examples/theme.json
---

# Slides

The above title should be orange and be prefixed with `CUSTOM`.


================================================
FILE: examples/import.md
================================================
This is just an example of how to import text from other files with
preprocess.md


================================================
FILE: examples/metadata.md
================================================
---
author: Gopher
date: May 22, 2022
paging: Page %d of %d
---

# Metadata Example

Customize the bottom information bar by adding metadata to your `slides.md` file.

```
--- 
author: Gopher
date: May 22, 2022
paging: Page %d of %d
--- 
```

---

# Metadata Example

You can also hide the bottom bar by leaving all of the fields blank

```
--- 
author: ""
date: ""
paging: ""
--- 
```


================================================
FILE: examples/preprocess.md
================================================
# Slides

You can add a code block with three tildes (~) and write a command to run before displaying
the slides, the text inside the code block will be passed as stdin to the command
and the code block will be replaced with the stdout of the command.

```
~~~graph-easy --as=boxart
[ A ] - to -> [ B ]
~~~
```

The above will be pre-processed to look like:

NOTE: You need `graph-easy` installed and in your `$PATH`

```
┌───┐  to   ┌───┐
│ A │ ────> │ B │
└───┘       └───┘
```

For security reasons, you must pass a file that has execution permissions
for the slides to be pre-processed.

```
chmod +x file.md
```

---

~~~sd replaced processed
This content will be passed in as stdin and will be replaced.
~~~

---


Any command will work

~~~echo "You can do whatever, really"
This doesn't matter, since it will be replaced by the stdout
of the command above because the command will disregard stdin.
~~~
---


You can use this to import snippets of code from other files:

~~~xargs cat
examples/import.md
~~~

---


## More pre-process examples:

### PlantUML

```
~~~plantuml -utxt -pipe
@startuml
A --> B: to
@enduml
~~~
```

The above will be pre-processed to look like:

NOTE: You need `plantuml` installed and in your `$PATH`

```
┌─┐          ┌─┐
│A│          │B│
└┬┘          └┬┘
 │    to      │
 │ ─ ─ ─ ─ ─ >│
┌┴┐          ┌┴┐
│A│          │B│
└─┘          └─┘



================================================
FILE: examples/slides.md
================================================
# Welcome to Slides
A terminal based presentation tool

```go
package main

import "fmt"

func main() {
  fmt.Println("Written in Go!")
}
```

---

## Everything is markdown
In fact this entire presentation is a markdown file

---

# h1
## h2
### h3
#### h4
##### h5
###### h6

---

# Markdown components
You can use everything in markdown!
* Like bulleted list
* You know the deal

1. Numbered lists too

---

# Tables

| Tables | Too    |
| ------ | ------ |
| Even   | Tables |

---

# Graphs

```
digraph {
    rankdir = LR;
    a -> b;
    b -> c;
}
```
```
┌───┐     ┌───┐     ┌───┐
│ a │ ──▶ │ b │ ──▶ │ c │
└───┘     └───┘     └───┘
```
---

All you need to do is separate slides with triple dashes
`---` on a separate line, like so:

```markdown
# Slide 1
Some stuff

--- 

# Slide 2
Some other stuff
```


================================================
FILE: examples/theme.json
================================================
{
  "document": {
    "block_prefix": "\n",
    "block_suffix": "\n",
    "color": "252",
    "margin": 2
  },
  "block_quote": {
    "indent": 1,
    "indent_token": "│ "
  },
  "paragraph": {},
  "list": {
    "level_indent": 2
  },
  "heading": {
    "block_suffix": "\n",
    "color": "39",
    "bold": true
  },
  "h1": {
    "prefix": "CUSTOM ",
    "suffix": " ",
    "color": "#fa0",
    "bold": true
  },
  "h2": {
    "prefix": "▓▓▓ ",
    "color": "#1cc"
  },
  "h3": {
    "prefix": "▒▒▒▒ ",
    "color": "#29c"
  },
  "h4": {
    "color": "#559",
    "prefix": "░░░░░ "
  },
  "h5": {},
  "h6": {},
  "text": {},
  "strikethrough": {
    "crossed_out": true
  },
  "emph": {
    "italic": true
  },
  "strong": {
    "bold": true
  },
  "hr": {
    "color": "240",
    "format": "\n--------\n"
  },
  "item": {
    "block_prefix": "• "
  },
  "enumeration": {
    "block_prefix": ". "
  },
  "task": {
    "ticked": "[✓] ",
    "unticked": "[ ] "
  },
  "link": {
    "color": "30",
    "underline": true
  },
  "link_text": {
    "color": "35",
    "bold": true
  },
  "image": {
    "color": "212",
    "underline": true
  },
  "image_text": {
    "color": "243",
    "format": "Image: {{.text}} →"
  },
  "code": {
    "prefix": " ",
    "suffix": " ",
    "color": "203",
    "background_color": "236"
  },
  "code_block": {
    "theme": "dracula",
    "margin": 2
  },
  "table": {
    "center_separator": "┼",
    "column_separator": "│",
    "row_separator": "─"
  },
  "definition_list": {},
  "definition_term": {},
  "definition_description": {
    "block_prefix": "\n🠶 "
  },
  "html_block": {},
  "html_span": {}
}


================================================
FILE: go.mod
================================================
module github.com/maaslalani/slides

go 1.22

require (
	github.com/atotto/clipboard v0.1.4
	github.com/charmbracelet/bubbles v0.18.0
	github.com/charmbracelet/bubbletea v0.26.2
	github.com/charmbracelet/glamour v0.7.0
	github.com/charmbracelet/lipgloss v0.10.0
	github.com/charmbracelet/ssh v0.0.0-20240507011153-ec70bd03034c
	github.com/charmbracelet/wish v1.4.0
	github.com/muesli/coral v1.0.0
	github.com/muesli/termenv v0.15.2
	github.com/stretchr/testify v1.9.0
	gopkg.in/yaml.v2 v2.4.0
)

require (
	github.com/alecthomas/chroma/v2 v2.8.0 // indirect
	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
	github.com/aymerick/douceur v0.2.0 // indirect
	github.com/charmbracelet/keygen v0.5.0 // indirect
	github.com/charmbracelet/log v0.4.0 // indirect
	github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 // indirect
	github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd // indirect
	github.com/creack/pty v1.1.21 // indirect
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/dlclark/regexp2 v1.4.0 // indirect
	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
	github.com/go-logfmt/logfmt v0.6.0 // indirect
	github.com/gorilla/css v1.0.0 // indirect
	github.com/inconshreveable/mousetrap v1.0.0 // indirect
	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
	github.com/mattn/go-isatty v0.0.18 // indirect
	github.com/mattn/go-localereader v0.0.1 // indirect
	github.com/mattn/go-runewidth v0.0.15 // indirect
	github.com/microcosm-cc/bluemonday v1.0.25 // indirect
	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
	github.com/muesli/cancelreader v0.2.2 // indirect
	github.com/muesli/reflow v0.3.0 // indirect
	github.com/olekukonko/tablewriter v0.0.5 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	github.com/rivo/uniseg v0.4.7 // indirect
	github.com/spf13/pflag v1.0.5 // indirect
	github.com/yuin/goldmark v1.5.4 // indirect
	github.com/yuin/goldmark-emoji v1.0.2 // indirect
	golang.org/x/crypto v0.21.0 // indirect
	golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
	golang.org/x/net v0.22.0 // indirect
	golang.org/x/sync v0.7.0 // indirect
	golang.org/x/sys v0.20.0 // indirect
	golang.org/x/term v0.20.0 // indirect
	golang.org/x/text v0.14.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)


================================================
FILE: go.sum
================================================
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264=
github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
github.com/charmbracelet/bubbletea v0.26.2 h1:Eeb+n75Om9gQ+I6YpbCXQRKHt5Pn4vMwusQpwLiEgJQ=
github.com/charmbracelet/bubbletea v0.26.2/go.mod h1:6I0nZ3YHUrQj7YHIHlM8RySX4ZIthTliMY+W8X8b+Gs=
github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng=
github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps=
github.com/charmbracelet/keygen v0.5.0 h1:XY0fsoYiCSM9axkrU+2ziE6u6YjJulo/b9Dghnw6MZc=
github.com/charmbracelet/keygen v0.5.0/go.mod h1:DfvCgLHxZ9rJxdK0DGw3C/LkV4SgdGbnliHcObV3L+8=
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/ssh v0.0.0-20240507011153-ec70bd03034c h1:nsxEhgGnHTGPh5qXr7EBHOKaaJ1nmQWIcI5TLRPYDqo=
github.com/charmbracelet/ssh v0.0.0-20240507011153-ec70bd03034c/go.mod h1:8/Ve8iGRRIGFM1kepYfRF2pEOF5Y3TEZYoJaA54228U=
github.com/charmbracelet/wish v1.4.0 h1:pL1uVP/YuYgJheHEj98teZ/n6pMYnmlZq/fcHvomrfc=
github.com/charmbracelet/wish v1.4.0/go.mod h1:ew4/MjJVfW/akEO9KmrQHQv1F7bQRGscRMrA+KtovTk=
github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 h1:3RXpZWGWTOeVXCTv0Dnzxdv/MhNUkBfEcbaTY0zrTQI=
github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd h1:HqBjkSFXXfW4IgX3TMKipWoPEN08T3Pi4SA/3DLss/U=
github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd/go.mod h1:6GZ13FjIP6eOCqWU4lqgveGnYxQo9c3qBzHPeFu4HBE=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/coral v1.0.0 h1:odyqkoEg4aJAINOzvnjN4tUsdp+Zleccs7tRIAkkYzU=
github.com/muesli/coral v1.0.0/go.mod h1:bf91M/dkp7iHQw73HOoR9PekdTJMTD6ihJgWoDitde8=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


================================================
FILE: internal/cmd/serve.go
================================================
package cmd

import (
	"context"
	"log"
	"os"
	"os/signal"
	"strconv"
	"syscall"
	"time"

	"github.com/maaslalani/slides/internal/model"
	"github.com/maaslalani/slides/internal/navigation"
	"github.com/maaslalani/slides/internal/server"
	"github.com/muesli/coral"
)

var (
	host     string
	port     int
	keyPath  string
	err      error
	fileName string
)

// ServeCmd is the command for serving the presentation. It starts the slides
// server allowing for connections.
var ServeCmd = &coral.Command{
	Use:     "serve <file.md>",
	Aliases: []string{"server"},
	Short:   "Start an SSH server to run slides",
	Args:    coral.ArbitraryArgs,
	RunE: func(cmd *coral.Command, args []string) error {
		k := os.Getenv("SLIDES_SERVER_KEY_PATH")
		if k != "" {
			keyPath = k
		}
		h := os.Getenv("SLIDES_SERVER_HOST")
		if h != "" {
			host = h
		}
		p := os.Getenv("SLIDES_SERVER_PORT")
		if p != "" {
			port, _ = strconv.Atoi(p)
		}

		if len(args) > 0 {
			fileName = args[0]
		}

		presentation := model.Model{
			Page:     0,
			Date:     time.Now().Format("2006-01-02"),
			FileName: fileName,
			Search:   navigation.NewSearch(),
		}
		err = presentation.Load()
		if err != nil {
			return err
		}

		s, err := server.NewServer(keyPath, host, port, presentation)
		if err != nil {
			return err
		}

		done := make(chan os.Signal, 1)
		signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
		log.Printf("Starting Slides server on %s:%d", host, port)
		go func() {
			if err = s.Start(); err != nil {
				log.Fatalln(err)
			}
		}()

		<-done
		log.Print("Stopping Slides server")
		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer func() { cancel() }()
		if err := s.Shutdown(ctx); err != nil {
			return err
		}
		return err
	},
}

func init() {
	ServeCmd.Flags().StringVar(&keyPath, "keyPath", "slides", "Server private key path")
	ServeCmd.Flags().StringVar(&host, "host", "localhost", "Server host to bind to")
	ServeCmd.Flags().IntVar(&port, "port", 53531, "Server port to bind to")
}


================================================
FILE: internal/code/code.go
================================================
package code

import (
	"errors"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"time"
)

// Block represents a code block.
type Block struct {
	Code     string
	Language string
}

// Result represents the output for an executed code block.
type Result struct {
	Out           string
	ExitCode      int
	ExecutionTime time.Duration
}

// ?: means non-capture group
var re = regexp.MustCompile("(?s)(?:```|~~~)(\\w+)\n(.*?)\n(?:```|~~~)\\s?")

var (
	// ErrParse is the returned error when we cannot parse the code block (i.e.
	// there is no code block on the current slide) or the code block is
	// incorrectly written.
	ErrParse = errors.New("Error: could not parse code block")
)

// Parse takes a block of markdown and returns an array of Block's with code
// and associated languages
func Parse(markdown string) ([]Block, error) {
	matches := re.FindAllStringSubmatch(markdown, -1)

	var rv []Block
	for _, match := range matches {
		// There was either no language specified or no code block
		// Either way, we cannot execute the expression
		if len(match) < 3 {
			continue
		}
		rv = append(rv, Block{
			Language: match[1],
			Code:     RemoveComments(match[2]),
		})

	}

	if len(rv) == 0 {
		return nil, ErrParse
	}

	return rv, nil
}

const (
	// ExitCodeInternalError represents the exit code in which the code
	// executing the code didn't work.
	ExitCodeInternalError = -1
)

// Execute takes a code.Block and returns the output of the executed code
func Execute(code Block) Result {
	// Check supported language
	language, ok := Languages[code.Language]
	if !ok {
		return Result{
			Out:      "Error: unsupported language",
			ExitCode: ExitCodeInternalError,
		}
	}

	// Write the code block to a temporary file
	f, err := os.CreateTemp(os.TempDir(), "slides-*."+Languages[code.Language].Extension)
	if err != nil {
		return Result{
			Out:      "Error: could not create file",
			ExitCode: ExitCodeInternalError,
		}
	}

	defer f.Close()
	defer os.Remove(f.Name())

	_, err = f.WriteString(code.Code)
	if err != nil {
		return Result{
			Out:      "Error: could not write to file",
			ExitCode: ExitCodeInternalError,
		}
	}

	var (
		output   strings.Builder
		exitCode int
	)

	// replacer for commands
	repl := strings.NewReplacer(
		"<file>", f.Name(),
		// <name>: file name without extension and without path
		"<name>", filepath.Base(strings.TrimSuffix(f.Name(), filepath.Ext(f.Name()))),
		"<path>", filepath.Dir(f.Name()),
	)

	// For accuracy of program execution speed, we can't put anything after
	// recording the start time or before recording the end time.
	start := time.Now()

	for _, c := range language.Commands {
		var command []string
		// replace <file>, <name> and <path> in commands
		for _, v := range c {
			command = append(command, repl.Replace(v))
		}
		// execute and write output
		cmd := exec.Command(command[0], command[1:]...)
		out, err := cmd.Output()
		if err != nil {
			output.Write([]byte(err.Error()))
		} else {
			output.Write(out)
		}

		// update status code
		if err != nil {
			if cmd.ProcessState != nil {
				exitCode = cmd.ProcessState.ExitCode()
			} else {
				exitCode = 1 // non-zero
			}
		}
	}

	end := time.Now()

	return Result{
		Out:           output.String(),
		ExitCode:      exitCode,
		ExecutionTime: end.Sub(start),
	}
}


================================================
FILE: internal/code/code_test.go
================================================
package code_test

import (
	"testing"

	"github.com/maaslalani/slides/internal/code"
)

func TestParse(t *testing.T) {
	tt := []struct {
		markdown string
		expected []code.Block
	}{
		// We can't put backticks ```
		// in multi-line strings, ~~~ instead
		{
			markdown: `
~~~ruby
puts "Hello, world!"
~~~
`,
			expected: []code.Block{
				{
					Code:     `puts "Hello, world!"`,
					Language: "ruby",
				},
			},
		},
		{
			markdown: `
~~~go
fmt.Println("Hello, world!")
~~~
`,
			expected: []code.Block{
				{
					Code:     `fmt.Println("Hello, world!")`,
					Language: "go",
				},
			},
		},
		{
			markdown: `
~~~python
print("Hello, world!")
~~~`,
			expected: []code.Block{
				{
					Code:     `print("Hello, world!")`,
					Language: "python",
				},
			},
		},
		{
			markdown: `
# Welcome to Slides

A terminal based presentation tool

~~~go
package main

import "fmt"

func main() {
  fmt.Println("Written in Go!")
}
~~~
`,
			expected: []code.Block{
				{
					Code: `package main

import "fmt"

func main() {
  fmt.Println("Written in Go!")
}`,
					Language: "go",
				},
			},
		},
		{
			markdown: `
# Slide 1
Just a regular slide, no code block
`,
			expected: nil,
		},
		{
			markdown: ``,
			expected: nil,
		},
		{
			markdown: `
~~~ruby
puts "Hello, world!"
~~~

~~~go
fmt.Println("Hello, world!")
~~~
`,
			expected: []code.Block{
				{
					Code:     `puts "Hello, world!"`,
					Language: "ruby",
				},
				{
					Code:     `fmt.Println("Hello, world!")`,
					Language: "go",
				},
			},
		},
	}

	for _, tc := range tt {
		blocks, _ := code.Parse(tc.markdown)
		if len(blocks) != len(tc.expected) {
			t.Errorf("parse fail: incorrect size of blocks")
		}
		for i, block := range blocks {
			expected := tc.expected[i]
			if block.Code != expected.Code {
				t.Log(block.Code)
				t.Log(expected.Code)
				t.Fatal("parse failed: incorrect code")
			}
			if block.Language != expected.Language {
				t.Fatalf("incorrect language, got %s, want %s", block.Language, expected.Language)
			}
		}
	}
}


================================================
FILE: internal/code/comments.go
================================================
package code

import (
	"regexp"
	"strings"
)

const comment = "///"

var commentRegexp = regexp.MustCompile("(?m)[\r\n]+^" + comment + ".*$")

// HideComments removes all comments from the given content.
func HideComments(content string) string {
	return commentRegexp.ReplaceAllString(content, "")
}

// RemoveComments strips all the comments from the given content.
// This is useful for when we want to actually use the content of the comments.
func RemoveComments(content string) string {
	return strings.ReplaceAll(content, comment, "")
}


================================================
FILE: internal/code/comments_test.go
================================================
package code

import "testing"

func TestHidesComments(t *testing.T) {
	content := `
///package main
///
///import "fmt"
///
///func main() {
  fmt.Println("Hello, world!")
///}`

	expected := `
  fmt.Println("Hello, world!")`

	if HideComments(content) != expected {
		t.Errorf("Expected %s, got %s", expected, HideComments(content))
	}
}

func TestNoComments(t *testing.T) {
	content := `
package main

import "fmt"

func main() {
  fmt.Println("Hello, world!")
}`
	expected := content

	if HideComments(content) != expected {
		t.Errorf("Expected %s, got %s", expected, HideComments(content))
	}
	if RemoveComments(content) != expected {
		t.Errorf("Expected %s, got %s", expected, HideComments(content))
	}
}

func TestRemoveComments(t *testing.T) {
	content := `
///package main
///
///import "fmt"
///
///func main() {
  fmt.Println("Hello, world!")
///}`

	expected := `
package main

import "fmt"

func main() {
  fmt.Println("Hello, world!")
}`

	if RemoveComments(content) != expected {
		t.Errorf("Expected %s, got %s", expected, RemoveComments(content))
	}
}


================================================
FILE: internal/code/execute_test.go
================================================
package code_test

import (
	"testing"

	"github.com/maaslalani/slides/internal/code"
)

func TestExecute(t *testing.T) {
	tt := []struct {
		block    code.Block
		expected code.Result
	}{
		{
			block: code.Block{
				Code: `
package main

import "fmt"

func main() {
  fmt.Print("Hello, go!")
}
        `,
				Language: "go",
			},
			expected: code.Result{
				Out:      "Hello, go!",
				ExitCode: 0,
			},
		},
		{
			block: code.Block{
				Code:     `echo "Hello, bash!"`,
				Language: "bash",
			},
			expected: code.Result{
				Out:      "Hello, bash!\n",
				ExitCode: 0,
			},
		},
		{
			block: code.Block{
				Code:     `Invalid Code`,
				Language: "bash",
			},
			expected: code.Result{
				Out:      "exit status 127",
				ExitCode: 127,
			},
		},
		{
			block: code.Block{
				Code:     `Invalid Code`,
				Language: "invalid",
			},
			expected: code.Result{
				Out:      "Error: unsupported language",
				ExitCode: code.ExitCodeInternalError,
			},
		},
	}

	for _, tc := range tt {
		r := code.Execute(tc.block)
		if r.Out != tc.expected.Out {
			t.Fatalf("invalid output for lang %s, got %s, want %s | %+v",
				tc.block.Language, r.Out, tc.expected.Out, r)
		}

		if r.ExitCode != tc.expected.ExitCode {
			t.Fatalf("unexpected exit code, got %d, want %d", r.ExitCode, tc.expected.ExitCode)
		}
	}
}


================================================
FILE: internal/code/languages.go
================================================
package code

// cmds: Multiple commands; placeholders can be used
// Placeholders <file>, <name> and <path> can be used.
type cmds [][]string

// Language represents a programming language with it Extension and Commands to
// execute its programs.
type Language struct {
	// Extension represents the file extension used by this language.
	Extension string
	// Commands  [][]string // placeholders: <name> file name (without
	// extension), <file> file name, <path> path without file name
	Commands cmds
}

// Supported Languages
const (
	Bash       = "bash"
	Zsh        = "zsh"
	Fish       = "fish"
	Elixir     = "elixir"
	Go         = "go"
	Javascript = "javascript"
	Lua        = "lua"
	OCaml      = "ocaml"
	Perl       = "perl"
	Python     = "python"
	Ruby       = "ruby"
	Rust       = "rust"
	Java       = "java"
	Julia      = "julia"
	Cpp        = "cpp"
	Swift      = "swift"
	Dart       = "dart"
	V          = "v"
	Scala      = "scala"
	Haskell    = "haskell"
)

// Languages is a map of supported languages with their extensions and commands
// to run to execute the program.
var Languages = map[string]Language{
	Bash: {
		Extension: "sh",
		Commands:  cmds{{"bash", "<file>"}},
	},
	Zsh: {
		Extension: "zsh",
		Commands:  cmds{{"zsh", "<file>"}},
	},
	Fish: {
		Extension: "fish",
		Commands:  cmds{{"fish", "<file>"}},
	},
	Elixir: {
		Extension: "exs",
		Commands:  cmds{{"elixir", "<file>"}},
	},
	Go: {
		Extension: "go",
		Commands:  cmds{{"go", "run", "<file>"}},
	},
	Javascript: {
		Extension: "js",
		Commands:  cmds{{"node", "<file>"}},
	},
	Lua: {
		Extension: "lua",
		Commands:  cmds{{"lua", "<file>"}},
	},
	Ruby: {
		Extension: "rb",
		Commands:  cmds{{"ruby", "<file>"}},
	},
	OCaml: {
		Extension: "ml",
		Commands:  cmds{{"ocaml", "<file>"}},
	},
	Python: {
		Extension: "py",
		Commands:  cmds{{"python", "<file>"}},
	},
	Perl: {
		Extension: "pl",
		Commands:  cmds{{"perl", "<file>"}},
	},
	Rust: {
		Extension: "rs",
		Commands: cmds{
			// compile code
			{"rustc", "<file>", "-o", "<path>/<name>.run"},
			// run compiled file
			{"<path>/<name>.run"},
		},
	},
	Java: {
		Extension: "java",
		Commands:  cmds{{"java", "<file>"}},
	},
	Julia: {
		Extension: "jl",
		Commands:  cmds{{"julia", "<file>"}},
	},
	Cpp: {
		Extension: "cpp",
		Commands: cmds{
			{"g++", "-std=c++20", "-o", "<path>/<name>.run", "<file>"},
			{"<path>/<name>.run"},
		},
	},
	Swift: {
		Extension: "swift",
		Commands:  cmds{{"swift", "<file>"}},
	},
	Dart: {
		Extension: "dart",
		Commands:  cmds{{"dart", "<file>"}},
	},
	V: {
		Extension: "v",
		Commands:  cmds{{"v", "run", "<file>"}},
	},
	Scala: {
		Extension: "sc",
		Commands: cmds{{"scala-cli", "run", "<file>"}},
	},
	Haskell: {
		Extension: "hs",
		Commands: cmds{{"runghc", "<file>"}},
	},
}


================================================
FILE: internal/file/file.go
================================================
// Package file includes utility functions
// for working with the filesystem
package file

import (
	"io/fs"
	"os"
)

// Exists is a helper to verify
// that the provided filepath exists
// on the system
func Exists(filepath string) bool {
	info, err := os.Stat(filepath)
	if os.IsNotExist(err) {
		return false
	}
	return !info.IsDir()
}

// IsExecutable returns whether a file has execution permissions
func IsExecutable(s fs.FileInfo) bool {
	return s.Mode().Perm()&0111 == 0111
}


================================================
FILE: internal/file/file_test.go
================================================
package file_test

import (
	"fmt"
	"io/fs"
	"os"
	"testing"

	"github.com/maaslalani/slides/internal/file"
	"github.com/stretchr/testify/assert"
)

func TestExists(t *testing.T) {
	tests := []struct {
		name     string
		filepath string
		want     bool
	}{
		{name: "Find file exists", filepath: "file.go", want: true},
		{name: "Return false for missing file", filepath: "afilethatdoesntexist.go", want: false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			isExist := file.Exists(tt.filepath)
			if isExist {
				assert.FileExists(t, tt.filepath)
			}
			assert.Equal(t, tt.want, isExist)
		})
	}
}

func TestIsExecutable(t *testing.T) {
	tests := []struct {
		perm     fs.FileMode
		expected bool
	}{
		{0101, false},
		{0111, true},
		{0644, false},
		{0666, false},
		{0777, true},
	}

	for _, tc := range tests {
		t.Run(fmt.Sprint(tc.perm), func(t *testing.T) {
			tmp, err := os.CreateTemp(os.TempDir(), "slides-*")
			if err != nil {
				t.Fatal("failed to create temp file")
			}
			defer os.Remove(tmp.Name())

			err = tmp.Chmod(tc.perm)
			if err != nil {
				t.Fatal(err)
			}

			s, err := tmp.Stat()
			if err != nil {
				t.Fatal("failed to stat file")
			}

			want := tc.expected
			got := file.IsExecutable(s)
			if tc.expected != got {
				t.Log(want)
				t.Log(got)
				t.Fatalf("IsExecutable returned an incorrect result, want: %t, got %t", want, got)
			}
		})
	}
}


================================================
FILE: internal/meta/meta.go
================================================
// Package meta implements markdown frontmatter parsing for simple
// slides configuration
package meta

import (
	"os"
	"os/user"
	"strings"
	"time"

	"gopkg.in/yaml.v2"
)

// Temporary structure to differentiate values not present in the YAML header
// from values set to empty strings in the YAML header. We replace values not
// set by defaults values when parsing a header.
type parsedMeta struct {
	Theme  *string `yaml:"theme"`
	Author *string `yaml:"author"`
	Date   *string `yaml:"date"`
	Paging *string `yaml:"paging"`
}

// Meta contains all of the data to be parsed
// out of a markdown file's header section
type Meta struct {
	Theme  string
	Author string
	Date   string
	Paging string
}

// New creates a new instance of the
// slideshow meta header object
func New() *Meta {
	return &Meta{}
}

// Parse parses metadata from a slideshows header slide
// including theme information
//
// If no front matter is provided, it will fallback to the default theme and
// return false to acknowledge that there is no front matter in this slide
func (m *Meta) Parse(header string) (*Meta, bool) {
	fallback := &Meta{
		Theme:  defaultTheme(),
		Author: defaultAuthor(),
		Date:   defaultDate(),
		Paging: defaultPaging(),
	}

	var tmp parsedMeta
	err := yaml.Unmarshal([]byte(header), &tmp)
	if err != nil {
		return fallback, false
	}

	if tmp.Theme != nil {
		m.Theme = *tmp.Theme
	} else {
		m.Theme = fallback.Theme
	}

	if tmp.Author != nil {
		m.Author = *tmp.Author
	} else {
		m.Author = fallback.Author
	}

	if tmp.Date != nil {
		parsedDate := parseDate(*tmp.Date)
		if parsedDate == *tmp.Date {
			m.Date = *tmp.Date
		} else {
			m.Date = time.Now().Format(parsedDate)
		}
	} else {
		m.Date = fallback.Date
	}

	if tmp.Paging != nil {
		m.Paging = *tmp.Paging
	} else {
		m.Paging = fallback.Paging
	}

	return m, true
}

func defaultTheme() string {
	theme := os.Getenv("GLAMOUR_STYLE")
	if theme == "" {
		return "default"
	}
	return theme
}

func defaultAuthor() string {
	user, err := user.Current()
	if err != nil {
		return ""
	}

	return user.Name
}

func defaultDate() string {
	return time.Now().Format(parseDate("YYYY-MM-DD"))
}

func defaultPaging() string {
	return "Slide %d / %d"
}

func parseDate(value string) string {
	pairs := [][]string{
		{"YYYY", "2006"},
		{"YY", "06"},
		{"MMMM", "January"},
		{"MMM", "Jan"},
		{"MM", "01"},
		{"mm", "1"},
		{"DD", "02"},
		{"dd", "2"},
	}

	for _, p := range pairs {
		value = strings.ReplaceAll(value, p[0], p[1])
	}
	return value
}


================================================
FILE: internal/meta/meta_test.go
================================================
package meta_test

import (
	"fmt"
	"os/user"
	"testing"
	"time"

	"github.com/maaslalani/slides/internal/meta"
	"github.com/stretchr/testify/assert"
)

func TestMeta_ParseHeader(t *testing.T) {
	user, _ := user.Current()
	date := time.Now().Format("2006-01-02")

	tests := []struct {
		name      string
		slideshow string
		want      *meta.Meta
	}{
		{
			name:      "Parse theme from header",
			slideshow: fmt.Sprintf("---\ntheme: %q\n", "dark"),
			want: &meta.Meta{
				Theme:  "dark",
				Author: user.Name,
				Date:   date,
				Paging: "Slide %d / %d",
			},
		},
		{
			name:      "Fallback to default if no theme provided",
			slideshow: "\n# Header Slide\n > Subtitle\n",
			want: &meta.Meta{
				Theme:  "default",
				Author: user.Name,
				Date:   date,
				Paging: "Slide %d / %d",
			},
		},
		{
			name:      "Parse author from header",
			slideshow: fmt.Sprintf("---\nauthor: %q\n", "gopher"),
			want: &meta.Meta{
				Theme:  "default",
				Author: "gopher",
				Date:   date,
				Paging: "Slide %d / %d",
			},
		},
		{
			name:      "Fallback to default if no author provided",
			slideshow: "\n# Header Slide\n > Subtitle\n",
			want: &meta.Meta{
				Theme:  "default",
				Author: user.Name,
				Date:   date,
				Paging: "Slide %d / %d",
			},
		},
		{
			name:      "Parse static date from header",
			slideshow: fmt.Sprintf("---\ndate: %q\n", "31/01/1970"),
			want: &meta.Meta{
				Theme:  "default",
				Author: user.Name,
				Date:   "31/01/1970",
				Paging: "Slide %d / %d",
			},
		},
		{
			name:      "Parse go-styled date from header",
			slideshow: fmt.Sprintf("---\ndate: %q\n", "MMM dd, YYYY"),
			want: &meta.Meta{
				Theme:  "default",
				Author: user.Name,
				Date:   time.Now().Format("Jan 2, 2006"),
				Paging: "Slide %d / %d",
			},
		},
		{
			name:      "Parse YYYY-MM-DD date from header",
			slideshow: fmt.Sprintf("---\ndate: %q\n", "YYYY-MM-DD"),
			want: &meta.Meta{
				Theme:  "default",
				Author: user.Name,
				Date:   time.Now().Format("2006-01-02"),
				Paging: "Slide %d / %d",
			},
		},
		{
			name:      "Parse dd/mm/YY date from header",
			slideshow: fmt.Sprintf("---\ndate: %q\n", "dd/mm/YY"),
			want: &meta.Meta{
				Theme:  "default",
				Author: user.Name,
				Date:   time.Now().Format("2/1/06"),
				Paging: "Slide %d / %d",
			},
		},
		{
			name:      "Parse MMM dd, YYYY date from header",
			slideshow: fmt.Sprintf("---\ndate: %q\n", "MMM dd, YYYY"),
			want: &meta.Meta{
				Theme:  "default",
				Author: user.Name,
				Date:   time.Now().Format("Jan 2, 2006"),
				Paging: "Slide %d / %d",
			},
		},
		{
			name:      "Parse MMMM DD, YYYY date from header",
			slideshow: fmt.Sprintf("---\ndate: %q\n", "MMMM DD, YYYY"),
			want: &meta.Meta{
				Theme:  "default",
				Author: user.Name,
				Date:   time.Now().Format("January 02, 2006"),
				Paging: "Slide %d / %d",
			},
		},
		{
			name:      "Fallback to default if no date provided",
			slideshow: "\n# Header Slide\n > Subtitle\n",
			want: &meta.Meta{
				Theme:  "default",
				Author: user.Name,
				Date:   date,
				Paging: "Slide %d / %d",
			},
		},
		{
			name:      "Parse paging from header",
			slideshow: fmt.Sprintf("---\npaging: %q\n", "%d of %d"),
			want: &meta.Meta{
				Theme:  "default",
				Author: user.Name,
				Date:   date,
				Paging: "%d of %d",
			},
		},
		{
			name:      "Fallback to default if no numebring provided",
			slideshow: "\n# Header Slide\n > Subtitle\n",
			want: &meta.Meta{
				Theme:  "default",
				Author: user.Name,
				Date:   date,
				Paging: "Slide %d / %d",
			},
		},
		{
			name:      "Fallback if first slide is valid yaml",
			slideshow: "---\n# Header Slide---\nContent\n",
			want: &meta.Meta{
				Theme:  "default",
				Author: user.Name,
				Date:   date,
				Paging: "Slide %d / %d",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			m := &meta.Meta{}
			got, hasMeta := m.Parse(tt.slideshow)
			if !hasMeta {
				assert.NotNil(t, got)
			}

			assert.Equal(t, tt.want, got)
		})
	}
}

func TestNew(t *testing.T) {
	tests := []struct {
		name string
		want *meta.Meta
	}{
		{name: "Create meta struct", want: &meta.Meta{}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, meta.New(), tt.want)
		})
	}
}

func ExampleMeta_Parse() {
	header := `
---
theme: "dark"
author: "Gopher"
date: "Apr. 4, 2021"
paging: "%d"
---
`
	// Parse the header from the markdown
	// file
	m, _ := meta.New().Parse(header)

	// Print the return theme
	// meta
	fmt.Println(m.Theme)
	fmt.Println(m.Author)
	fmt.Println(m.Date)
	fmt.Println(m.Paging)
}


================================================
FILE: internal/model/model.go
================================================
package model

import (
	"bufio"
	_ "embed"
	"errors"
	"fmt"
	"io"
	"os"
	"strings"
	"time"

	"github.com/atotto/clipboard"
	"github.com/maaslalani/slides/internal/file"
	"github.com/maaslalani/slides/internal/navigation"
	"github.com/maaslalani/slides/internal/process"

	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/glamour"
	"github.com/maaslalani/slides/internal/code"
	"github.com/maaslalani/slides/internal/meta"
	"github.com/maaslalani/slides/styles"
)

var (
	//go:embed tutorial.md
	slidesTutorial []byte
	tabSpaces      = strings.Repeat(" ", 4)
)

const (
	delimiter = "\n---\n"
)

// Model represents the model of this presentation, which contains all the
// state related to the current slides.
type Model struct {
	Slides   []string
	Page     int
	Author   string
	Date     string
	Theme    glamour.TermRendererOption
	Paging   string
	FileName string
	viewport viewport.Model
	buffer   string
	// VirtualText is used for additional information that is not part of the
	// original slides, it will be displayed on a slide and reset on page change
	VirtualText string
	Search      navigation.Search
}

type fileWatchMsg struct{}

var fileInfo os.FileInfo

// Init initializes the model and begins watching the slides file for changes
// if it exists.
func (m Model) Init() tea.Cmd {
	if m.FileName == "" {
		return nil
	}
	fileInfo, _ = os.Stat(m.FileName)
	return fileWatchCmd()
}

func fileWatchCmd() tea.Cmd {
	return tea.Every(time.Second, func(t time.Time) tea.Msg {
		return fileWatchMsg{}
	})
}

// Load loads all of the content and metadata for the presentation.
func (m *Model) Load() error {
	var content string
	var err error

	if m.FileName != "" {
		content, err = readFile(m.FileName)
	} else {
		content, err = readStdin()
	}

	if err != nil {
		return err
	}

	content = strings.ReplaceAll(content, "\r", "")

	content = strings.TrimPrefix(content, strings.TrimPrefix(delimiter, "\n"))
	slides := strings.Split(content, delimiter)

	metaData, exists := meta.New().Parse(slides[0])
	// If the user specifies a custom configuration options
	// skip the first "slide" since this is all configuration
	if exists && len(slides) > 1 {
		slides = slides[1:]
	}

	m.Slides = slides
	m.Author = metaData.Author
	m.Date = metaData.Date
	m.Paging = metaData.Paging
	if m.Theme == nil {
		m.Theme = styles.SelectTheme(metaData.Theme)
	}

	return nil
}

// Update updates the presentation model.
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.viewport.Width = msg.Width
		m.viewport.Height = msg.Height
		return m, nil

	case tea.KeyMsg:
		keyPress := msg.String()

		if m.Search.Active {
			switch msg.Type {
			case tea.KeyEnter:
				// execute current buffer
				if m.Search.Query() != "" {
					m.Search.Execute(&m)
				} else {
					m.Search.Done()
				}
				// cancel search
				return m, nil
			case tea.KeyCtrlC, tea.KeyEscape:
				// quit command mode
				m.Search.SetQuery("")
				m.Search.Done()
				return m, nil
			}

			var cmd tea.Cmd
			m.Search.SearchTextInput, cmd = m.Search.SearchTextInput.Update(msg)
			return m, cmd
		}

		switch keyPress {
		case "/":
			// Begin search
			m.Search.Begin()
			m.Search.SearchTextInput.Focus()
			return m, nil
		case "ctrl+n":
			// Go to next occurrence
			m.Search.Execute(&m)
		case "ctrl+e":
			// Run code blocks
			blocks, err := code.Parse(m.Slides[m.Page])
			if err != nil {
				// We couldn't parse the code block on the screen
				m.VirtualText = "\n" + err.Error()
				return m, nil
			}
			var outs []string
			for _, block := range blocks {
				res := code.Execute(block)
				outs = append(outs, res.Out)
			}
			m.VirtualText = strings.Join(outs, "\n")
		case "y":
			blocks, err := code.Parse(m.Slides[m.Page])
			if err != nil {
				return m, nil
			}
			for _, b := range blocks {
				_ = clipboard.WriteAll(b.Code)
			}
			return m, nil
		case "ctrl+c", "q":
			return m, tea.Quit
		default:
			newState := navigation.Navigate(navigation.State{
				Buffer:      m.buffer,
				Page:        m.Page,
				TotalSlides: len(m.Slides),
			}, keyPress)
			m.buffer = newState.Buffer
			m.SetPage(newState.Page)
		}

	case fileWatchMsg:
		newFileInfo, err := os.Stat(m.FileName)
		if err == nil && newFileInfo.ModTime() != fileInfo.ModTime() {
			fileInfo = newFileInfo
			_ = m.Load()
			if m.Page >= len(m.Slides) {
				m.Page = len(m.Slides) - 1
			}
		}
		return m, fileWatchCmd()
	}
	return m, nil
}

// View renders the current slide in the presentation and the status bar which
// contains the author, date, and pagination information.
func (m Model) View() string {
	r, _ := glamour.NewTermRenderer(m.Theme, glamour.WithWordWrap(m.viewport.Width))
	slide := m.Slides[m.Page]
	slide = code.HideComments(slide)
	slide, err := r.Render(slide)
	slide = strings.ReplaceAll(slide, "\t", tabSpaces)
	slide += m.VirtualText
	if err != nil {
		slide = fmt.Sprintf("Error: Could not render markdown! (%v)", err)
	}
	slide = styles.Slide.Render(slide)

	var left string
	if m.Search.Active {
		// render search bar
		left = m.Search.SearchTextInput.View()
	} else {
		// render author and date
		left = styles.Author.Render(m.Author) + styles.Date.Render(m.Date)
	}

	right := styles.Page.Render(m.paging())
	status := styles.Status.Render(styles.JoinHorizontal(left, right, m.viewport.Width))
	return styles.JoinVertical(slide, status, m.viewport.Height)
}

func (m *Model) paging() string {
	switch strings.Count(m.Paging, "%d") {
	case 2:
		return fmt.Sprintf(m.Paging, m.Page+1, len(m.Slides))
	case 1:
		return fmt.Sprintf(m.Paging, m.Page+1)
	default:
		return m.Paging
	}
}

func readFile(path string) (string, error) {
	s, err := os.Stat(path)
	if err != nil {
		return "", errors.New("could not read file")
	}
	if s.IsDir() {
		return "", errors.New("can not read directory")
	}
	b, err := os.ReadFile(path)
	if err != nil {
		return "", err
	}
	content := string(b)

	// Pre-process slides if the file is executable to avoid
	// unintentional code execution when presenting slides
	if file.IsExecutable(s) {
		// Remove shebang if file has one
		if strings.HasPrefix(content, "#!") {
			content = strings.Join(strings.SplitN(content, "\n", 2)[1:], "\n")
		}

		content = process.Pre(content)
	}

	return content, err
}

func readStdin() (string, error) {
	stat, err := os.Stdin.Stat()
	if err != nil {
		return "", err
	}

	if stat.Mode()&os.ModeNamedPipe == 0 && stat.Size() == 0 {
		return string(slidesTutorial), nil
	}

	reader := bufio.NewReader(os.Stdin)
	var b strings.Builder

	for {
		r, _, err := reader.ReadRune()
		if err != nil && err == io.EOF {
			break
		}
		_, err = b.WriteRune(r)
		if err != nil {
			return "", err
		}
	}

	return b.String(), nil
}

// CurrentPage returns the current page the presentation is on.
func (m *Model) CurrentPage() int {
	return m.Page
}

// SetPage sets which page the presentation should render.
func (m *Model) SetPage(page int) {
	if m.Page == page {
		return
	}

	m.VirtualText = ""
	m.Page = page
}

// Pages returns all the slides in the presentation.
func (m *Model) Pages() []string {
	return m.Slides
}


================================================
FILE: internal/model/tutorial.md
================================================
# Welcome to Slides
A terminal based presentation tool

## Everything is markdown
In fact this entire presentation is a markdown file.

Press `n` to go to the next slide.

---

# Display Code

```go
package main

import "fmt"

func main() {
  // You can show code in slides
  // Press ctrl+e to execute this code directly in slides
  fmt.Println("Tada!")
}
```

---

# h1

You can use everything in markdown!
* Like bulleted list
* You know the deal

1. Numbered lists too

## h2

| Tables | Too    |
| ------ | ------ |
| Even   | Tables |


### h3

#### h4
##### h5
###### h6

---

# Graphs

```
digraph {
    rankdir = LR;
    a -> b;
    b -> c;
}
```
```
┌───┐     ┌───┐     ┌───┐
│ a │ ──▶ │ b │ ──▶ │ c │
└───┘     └───┘     └───┘
```
---

All you need to do is separate slides with triple dashes
`---` on a separate line, like so:

```markdown
# Slide 1
Some stuff

--- 

# Slide 2
Some other stuff
```


================================================
FILE: internal/navigation/navigation.go
================================================
package navigation

import (
	"strconv"
)

type repeatableFunc func(slide, totalSlides int) int

// State tracks the current buffer, page, and total number of slides
type State struct {
	Buffer      string
	Page        int
	TotalSlides int
}

// Navigate receives the current State and keyPress, and returns the new State.
func Navigate(state State, keyPress string) State {
	switch keyPress {
	case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
		newBuffer := keyPress

		if bufferIsNumeric(state.Buffer) {
			newBuffer = state.Buffer + keyPress
		}

		return State{
			Buffer:      newBuffer,
			Page:        state.Page,
			TotalSlides: state.TotalSlides,
		}
	case "g":
		switch state.Buffer {
		case "g":
			return State{
				Page:        0,
				TotalSlides: state.TotalSlides,
			}
		default:
			return State{
				Buffer:      "g",
				Page:        state.Page,
				TotalSlides: state.TotalSlides,
			}
		}
	case "G":
		targetSlide := state.TotalSlides - 1
		if bufferIsNumeric(state.Buffer) {
			targetSlide = navigateSlide(state.Buffer, state.TotalSlides)
		}

		return State{
			Page:        targetSlide,
			TotalSlides: state.TotalSlides,
		}
	case " ", "down", "j", "right", "l", "enter", "n", "pgdown":
		return State{
			Page:        navigateNext(state),
			TotalSlides: state.TotalSlides,
		}
	case "up", "k", "left", "h", "p", "pgup", "N":
		return State{
			Page:        navigatePrevious(state),
			TotalSlides: state.TotalSlides,
		}
	default:
		return State{
			Page:        state.Page,
			TotalSlides: state.TotalSlides,
		}
	}
}

func bufferIsNumeric(buffer string) bool {
	_, err := strconv.Atoi(buffer)
	return err == nil
}

func navigateNext(state State) int {
	return repeatableAction(func(slide, totalSlides int) int {
		if slide < totalSlides-1 {
			return slide + 1
		}

		return totalSlides - 1
	}, state)
}

func navigateSlide(buffer string, totalSlides int) int {
	destinationSlide, _ := strconv.Atoi(buffer)
	destinationSlide--

	if destinationSlide > totalSlides-1 {
		return totalSlides - 1
	}

	if destinationSlide < 0 {
		return 0
	}

	return destinationSlide
}

func navigatePrevious(state State) int {
	return repeatableAction(func(slide, totalSlides int) int {
		if slide > 0 {
			return slide - 1
		}

		return slide
	}, state)
}

func repeatableAction(fn repeatableFunc, state State) int {
	if !bufferIsNumeric(state.Buffer) {
		return fn(state.Page, state.TotalSlides)
	}

	repeat, _ := strconv.Atoi(state.Buffer)
	page := state.Page

	if repeat == 0 {
		// This is how behaviour works in Vim, so following principle of least astonishment.
		return fn(state.Page, state.TotalSlides)
	}

	for i := 0; i < repeat; i++ {
		page = fn(page, state.TotalSlides)
	}

	return page
}


================================================
FILE: internal/navigation/navigation_test.go
================================================
package navigation

import (
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestNavigation(t *testing.T) {
	tests := []struct {
		keys   string
		target int
	}{
		{target: 0},
		{keys: "l", target: 1},
		{keys: "jjjjjjjjjj", target: 10},
		{keys: "jjjjjjjjjjjjj", target: 10},
		{keys: "G", target: 10},
		{keys: "llgg", target: 0},
		{keys: "2j", target: 2},
		{keys: "0j", target: 1},
		{keys: "-11G", target: 10},
		{keys: "0G", target: 0},
		{keys: "3G", target: 2},
		{keys: "11G", target: 10},
		{keys: "101G", target: 10},
		{keys: "nnN", target: 1},
	}

	for _, tt := range tests {
		t.Run(tt.keys, func(t *testing.T) {
			currentState := State{
				Buffer:      "",
				Page:        0,
				TotalSlides: 11,
			}

			for _, key := range strings.Split(tt.keys, "") {
				currentState = Navigate(currentState, key)
			}

			targetState := State{Page: tt.target, TotalSlides: 11}
			assert.Equal(t, targetState, currentState)
		})
	}
}


================================================
FILE: internal/navigation/search.go
================================================
package navigation

import (
	"regexp"
	"strings"

	"github.com/charmbracelet/bubbles/textinput"
	"github.com/maaslalani/slides/styles"
)

// Model is an interface for models.model, so that cycle imports are avoided
type Model interface {
	CurrentPage() int
	SetPage(page int)
	Pages() []string
}

// Search represents the current search
type Search struct {
	// Active - Show search bar instead of author and date?
	// Store keystrokes in Query?
	Active bool
	// Query stores the current "search term"
	SearchTextInput textinput.Model
}

// NewSearch creates and returns a new search model with the default settings.
func NewSearch() Search {
	ti := textinput.New()
	ti.Placeholder = "search"
	ti.Prompt = "/"
	ti.PromptStyle = styles.Search
	ti.TextStyle = styles.Search
	return Search{SearchTextInput: ti}
}

// Query returns the text input's value.
func (s *Search) Query() string {
	return s.SearchTextInput.Value()
}

// SetQuery sets the text input's value
func (s *Search) SetQuery(query string) {
	s.SearchTextInput.SetValue(query)
}

// Done marks the search as done, but does not delete the search buffer. This
// is useful if, for example, you want to jump to the next result and you
// therefore still need the buffer.
func (s *Search) Done() {
	s.Active = false
}

// Begin a new search (deletes old buffer)
func (s *Search) Begin() {
	s.Active = true
	s.SetQuery("")
}

// Execute search
func (s *Search) Execute(m Model) {
	defer s.Done()
	expr := s.Query()
	if expr == "" {
		return
	}
	if strings.HasSuffix(expr, "/i") {
		expr = "(?i)" + expr[:len(expr)-2]
	}
	pattern, err := regexp.Compile(expr)
	if err != nil {
		return
	}
	check := func(i int) bool {
		content := m.Pages()[i]
		if len(pattern.FindAllStringSubmatch(content, 1)) != 0 {
			m.SetPage(i)
			return true
		}
		return false
	}
	// search from next slide to end
	for i := m.CurrentPage() + 1; i < len(m.Pages()); i++ {
		if check(i) {
			return
		}
	}
	// search from first slide to previous
	for i := 0; i < m.CurrentPage(); i++ {
		if check(i) {
			return
		}
	}
}


================================================
FILE: internal/navigation/search_test.go
================================================
package navigation

import (
	"testing"
)

type mockModel struct {
	slides []string
	page   int
}

func (m *mockModel) CurrentPage() int {
	return m.page
}

func (m *mockModel) SetPage(page int) {
	m.page = page
}

func (m *mockModel) Pages() []string {
	return m.slides
}

func TestSearch(t *testing.T) {
	data := []string{
		"hi",
		"first",
		"second",
		"third",
		"AbCdEfG",
		"abcdefg",
		"seconds",
	}

	type query struct {
		desc     string
		query    string
		expected int
	}

	// query -> expected page
	queries := []query{
		{"basic 'first'", "first", 1},
		{"basic 'abc'", "abc", 5},
		{"basic 'abc' next occurrence", "abc", 5},
		{"'abc' ignore case", "abc/i", 4},
		{"'abc' ignore case", "abc/i", 5},
		{"'abc' ignore case", "abc/i", 4},
		{"next occurrence 1/2", "sec", 6},
		{"next occurrence 2/2", "sec", 2},
		{"regex", "a.c", 5},
		{"regex next occurrence", "a.c", 5},
		{"regex ignore case", "a.c/i", 4},
		{"regex ignore case next occurrence", "a.c/i", 5},
	}

	m := &mockModel{
		slides: data,
		page:   0,
	}

	s := &Search{}
	for _, query := range queries {
		s.SetQuery(query.query)
		s.Execute(m)
		if m.CurrentPage() != query.expected {
			t.Errorf("[%s] expected page %d, got %d", query.desc, query.expected, m.CurrentPage())
		}
	}

}


================================================
FILE: internal/process/execute_test.go
================================================
package process

import "testing"

func TestExecute(t *testing.T) {
	tt := []struct {
		block Block
		want  string
	}{
		{
			block: Block{
				Command: "cat",
				Input:   "Hello, world!",
			},
			want: "Hello, world!",
		},
		{
			block: Block{
				Command: "sed -e s/Find/Replace/g",
				Input:   "Find",
			},
			want: "Replace",
		},
	}

	for _, tc := range tt {
		t.Run(tc.want, func(t *testing.T) {
			if testing.Short() {
				t.SkipNow()
			}
			tc.block.Execute()
			got := tc.block.Output
			if tc.want != got {
				t.Fatalf("Invalid execution, want %s, got %s", tc.want, got)
			}
		})
	}
}


================================================
FILE: internal/process/process.go
================================================
package process

import (
	"fmt"
	"io"
	"os/exec"
	"regexp"
	"strings"
)

// Block represents a pre-processable block which looks like the following: It
// is delimited by ~~~ and contains a command to be run along with the input to
// be passed, the entire block should be replaced with its command output
//
// ~~~sd block process
// block
// ~~~
type Block struct {
	Command string
	Input   string
	Output  string
	Raw     string
}

// String implements the Stringer interface.
func (b Block) String() string {
	return fmt.Sprintf("===\n%s\n%s\n%s\n===", b.Raw, b.Command, b.Input)
}

// ?: means non-capture group
var reng = regexp.MustCompile("~~~(.+)\n(?:.|\n)*?\n~~~\\s?")
var reg = regexp.MustCompile("(?s)~~~(.+?)\n(.*?)\n~~~\\s?")

// Parse takes some markdown and returns blocks to be pre-processed
func Parse(markdown string) []Block {
	var blocks []Block
	matches := reng.FindAllString(markdown, -1)
	for _, match := range matches {
		m := reg.FindStringSubmatch(match)
		blocks = append(blocks, Block{
			Command: m[1],
			Input:   m[2],
			Raw:     strings.TrimSuffix(m[0], "\n"),
		})
	}
	return blocks
}

// Execute takes performs the execution of the block's command
// by passing in the block's input as stdin and sets the block output
func (b *Block) Execute() {
	c := strings.Split(b.Command, " ")
	cmd := exec.Command(c[0], c[1:]...)
	stdin, err := cmd.StdinPipe()
	if err != nil {
		return
	}

	go func() {
		defer stdin.Close()
		_, _ = io.WriteString(stdin, b.Input)
	}()

	out, err := cmd.Output()
	if err != nil {
		return
	}

	b.Output = string(out)
}

// Pre processes the markdown content by executing the commands necessary and
// returns the new processed content
func Pre(content string) string {
	blocks := Parse(content)

	if len(blocks) <= 0 {
		return content
	}

	for _, block := range blocks {
		// TODO: Use goroutines, if possible
		block.Execute()

		// If multiple blocks have the same Raw value The will _likely_ have the
		// same Output value so we can probably optimize this
		// There may be edge cases, though, since block execution is not deterministic.
		content = strings.Replace(content, block.Raw, block.Output, 1)
	}
	return content
}


================================================
FILE: internal/process/process_test.go
================================================
package process

import (
	"reflect"
	"testing"
)

func TestParse(t *testing.T) {
	md := `
# Slide

~~~sd Replace Process
Replace
~~~

Hello

~~~sd Replace Process
Replace
Multi-line input
~~~

~~~echo -n World
Hello
~~~

---

# Next Slide

GraphViz Test

~~~graph-easy --as=boxart
digraph {
  A -> B
}
~~~
`

	got := Parse(md)
	want := []Block{{
		Command: "sd Replace Process",
		Input:   "Replace",
		Raw:     "~~~sd Replace Process\nReplace\n~~~",
	}, {
		Command: "sd Replace Process",
		Input:   "Replace\nMulti-line input",
		Raw:     "~~~sd Replace Process\nReplace\nMulti-line input\n~~~",
	}, {
		Command: "echo -n World",
		Input:   "Hello",
		Raw:     "~~~echo -n World\nHello\n~~~",
	}, {
		Command: "graph-easy --as=boxart",
		Input:   "digraph {\n  A -> B\n}",
		Raw:     "~~~graph-easy --as=boxart\ndigraph {\n  A -> B\n}\n~~~",
	}}

	if !reflect.DeepEqual(got, want) {
		t.Log(want)
		t.Log(got)
		t.Fatal("Did not parse blocks correctly")
	}
}


================================================
FILE: internal/server/middleware.go
================================================
package server

import (
	"fmt"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/ssh"
	"github.com/charmbracelet/wish"
	bm "github.com/charmbracelet/wish/bubbletea"
	"github.com/muesli/termenv"
)

func slidesMiddleware(srv *Server) wish.Middleware {
	newProg := func(m tea.Model, opts ...tea.ProgramOption) *tea.Program {
		p := tea.NewProgram(m, opts...)
		return p
	}
	teaHandler := func(s ssh.Session) *tea.Program {
		_, _, active := s.Pty()
		if !active {
			fmt.Println("no active terminal, skipping")
			err := s.Exit(1)
			if err != nil {
				fmt.Println("Error exiting session")
			}
			return nil
		}
		return newProg(srv.presentation, tea.WithInput(s), tea.WithOutput(s), tea.WithAltScreen())
	}
	return bm.MiddlewareWithProgramHandler(teaHandler, termenv.ANSI256)
}


================================================
FILE: internal/server/server.go
================================================
package server

import (
	"context"
	"fmt"

	"github.com/charmbracelet/ssh"
	"github.com/charmbracelet/wish"
	"github.com/maaslalani/slides/internal/model"
)

// Server is the server for hosting this presentation.
type Server struct {
	host         string
	port         int
	srv          *ssh.Server
	presentation model.Model
}

// NewServer creates a new server.
func NewServer(keyPath, host string, port int, presentation model.Model) (*Server, error) {
	s := &Server{
		host:         host,
		port:         port,
		presentation: presentation,
	}
	srv, err := wish.NewServer(
		wish.WithHostKeyPath(keyPath),
		wish.WithAddress(fmt.Sprintf("%s:%d", host, port)),
		wish.WithMiddleware(
			slidesMiddleware(s),
		),
	)
	if err != nil {
		return nil, err
	}
	s.srv = srv
	return s, nil
}

// Start starts the ssh server.
func (s *Server) Start() error {
	return s.srv.ListenAndServe()
}

// Shutdown shuts down the server.
func (s *Server) Shutdown(ctx context.Context) error {
	return s.srv.Shutdown(ctx)
}


================================================
FILE: main.go
================================================
package main

import (
	_ "embed"
	"os"
	"time"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/maaslalani/slides/internal/cmd"
	"github.com/maaslalani/slides/internal/model"
	"github.com/maaslalani/slides/internal/navigation"
	"github.com/muesli/coral"
)

var (
	rootCmd = &coral.Command{
		Use:   "slides <file.md>",
		Short: "Terminal based presentation tool",
		Args:  coral.ArbitraryArgs,
		RunE: func(cmd *coral.Command, args []string) error {
			var err error
			var fileName string

			if len(args) > 0 {
				fileName = args[0]
			}

			presentation := model.Model{
				Page:     0,
				Date:     time.Now().Format("2006-01-02"),
				FileName: fileName,
				Search:   navigation.NewSearch(),
			}
			err = presentation.Load()
			if err != nil {
				return err
			}

			p := tea.NewProgram(presentation, tea.WithAltScreen())
			_, err = p.Run()
			return err
		},
	}
)

func init() {
	rootCmd.AddCommand(
		cmd.ServeCmd,
	)
	rootCmd.CompletionOptions.DisableDefaultCmd = true
}

func main() {
	if err := rootCmd.Execute(); err != nil {
		os.Exit(1)
	}
}


================================================
FILE: snap/snapcraft.yaml
================================================
name: slides
adopt-info: slides
summary: Slides in your terminal.
description: |
  Slides in your terminal.
  
  Usage:
    slides <file.md> [flags]

  Flags:
    -h, --help   help for slides
    
license: MIT

base: core22
grade: stable 
confinement: strict
compression: lzo

architectures:
  - build-on: amd64
  - build-on: arm64
  - build-on: armhf
  - build-on: ppc64el
  - build-on: s390x
  
assumes:
  - command-chain
  
apps:
  slides:
    command: bin/slides
    command-chain: 
      - bin/homeishome-launch     
    plugs:
      - home
      - ssh-keys
      - ssh-public-keys
      - network
      - network-bind
      
parts:
  slides:
    source: https://github.com/maaslalani/slides
    source-type: git
    plugin: go
    build-snaps:
      - go
      
    override-pull: |
      snapcraftctl pull
      snapcraftctl set-version "$(git describe --tags | sed 's/^v//' | cut -d "-" -f1)"    

  homeishome-launch:
    plugin: nil
    stage-snaps:
      - homeishome-launch     


================================================
FILE: styles/styles.go
================================================
// Package styles implements the theming logic for slides
package styles

import (
	_ "embed"
	"io"
	"net/http"
	"os"
	"strings"

	"github.com/charmbracelet/glamour"
	"github.com/charmbracelet/lipgloss"
	"github.com/muesli/termenv"
)

const (
	salmon = lipgloss.Color("#E8B4BC")
)

var (
	// Author is the style for the author text in the bottom-left corner of the
	// presentation.
	Author = lipgloss.NewStyle().Foreground(salmon).Align(lipgloss.Left).MarginLeft(2)
	// Date is the style for the date text in the bottom-left corner of the
	// presentation.
	Date = lipgloss.NewStyle().Faint(true).Align(lipgloss.Left).Margin(0, 1)
	// Page is the style for the pagination progress information text in the
	// bottom-right corner of the presentation.
	Page = lipgloss.NewStyle().Foreground(salmon).Align(lipgloss.Right).MarginRight(3)
	// Slide is the style for the slide.
	Slide = lipgloss.NewStyle().Padding(1)
	// Status is the style for the status bar at the bottom of the
	// presentation.
	Status = lipgloss.NewStyle().Padding(1)
	// Search is the style for the search input at the bottom-left corner of
	// the screen when searching is active.
	Search = lipgloss.NewStyle().Faint(true).Align(lipgloss.Left).MarginLeft(2)
)

var (
	// DefaultTheme is the default theme for the presentation.
	//go:embed theme.json
	DefaultTheme []byte
)

// JoinHorizontal joins two strings horizontally and fills the space in-between.
func JoinHorizontal(left, right string, width int) string {
	w := width - lipgloss.Width(right)
	return lipgloss.PlaceHorizontal(w, lipgloss.Left, left) + right
}

// JoinVertical joins two strings vertically and fills the space in-between.
func JoinVertical(top, bottom string, height int) string {
	h := height - lipgloss.Height(bottom)
	return lipgloss.PlaceVertical(h, lipgloss.Top, top) + bottom
}

// SelectTheme picks a glamour style config based
// on the theme provided in the markdown header
func SelectTheme(theme string) glamour.TermRendererOption {
	switch theme {
	case "ascii":
		return glamour.WithStyles(glamour.ASCIIStyleConfig)
	case "light":
		return glamour.WithStyles(glamour.LightStyleConfig)
	case "dark":
		return glamour.WithStyles(glamour.DarkStyleConfig)
	case "notty":
		return glamour.WithStyles(glamour.NoTTYStyleConfig)
	default:
		var themeReader io.Reader
		var err error
		if strings.HasPrefix(theme, "http") {
			var resp *http.Response
			resp, err = http.Get(theme)
			if err != nil {
				return getDefaultTheme()
			}
			defer resp.Body.Close()
			themeReader = resp.Body
		} else {
			file, err := os.Open(theme)
			if err != nil {
				return getDefaultTheme()
			}
			defer file.Close()
			themeReader = file
		}
		bytes, err := io.ReadAll(themeReader)
		if err == nil {
			return glamour.WithStylesFromJSONBytes(bytes)
		}
		// Should log a warning so the user knows we failed to read their theme file
		return getDefaultTheme()
	}
}

func getDefaultTheme() glamour.TermRendererOption {
	if termenv.EnvNoColor() {
		return glamour.WithStyles(glamour.NoTTYStyleConfig)
	}

	if !termenv.HasDarkBackground() {
		return glamour.WithStyles(glamour.LightStyleConfig)
	}

	return glamour.WithStylesFromJSONBytes(DefaultTheme)
}


================================================
FILE: styles/styles_test.go
================================================
package styles_test

import (
	"testing"

	"github.com/charmbracelet/glamour"
	"github.com/charmbracelet/glamour/ansi"
	"github.com/maaslalani/slides/styles"
	"github.com/stretchr/testify/assert"
)

func TestSelectTheme(t *testing.T) {
	tests := []struct {
		name    string
		theme   string
		want    ansi.StyleConfig
		wantErr bool
	}{
		{name: "Select dark theme", theme: "dark", want: glamour.DarkStyleConfig, wantErr: false},
		{name: "Select light theme", theme: "light", want: glamour.LightStyleConfig, wantErr: false},
		{name: "Select ascii theme", theme: "ascii", want: glamour.ASCIIStyleConfig, wantErr: false},
		{name: "Select notty theme", theme: "notty", want: glamour.NoTTYStyleConfig, wantErr: false},
		{name: "Select theme with error", theme: "notty", want: glamour.DarkStyleConfig, wantErr: true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Execute the theme selection and ensure
			// it returns a non-nil theme
			selectedTheme := styles.SelectTheme(tt.theme)
			assert.NotNil(t, selectedTheme)

			// Initialize renderers to compare output
			gotRenderer, _ := glamour.NewTermRenderer(selectedTheme)
			wantRenderer, _ := glamour.NewTermRenderer(glamour.WithStyles(tt.want))

			// Render a the same string with two different
			// renderers
			gotOutput, _ := gotRenderer.Render(tt.name)
			wantOutput, _ := wantRenderer.Render(tt.name)

			// Inject exception to ensure a style that doesn't match
			// it's associated string
			if tt.wantErr {
				assert.NotEqual(t, wantOutput, gotOutput)
				return
			}

			// Ensure they both match
			assert.Equal(t, wantOutput, gotOutput)
		})
	}
}

func TestSelectTheme_file(t *testing.T) {
	tests := []struct {
		name       string
		theme      string
		fileExists bool
	}{
		{name: "Select custom theme json", theme: "./theme.json", fileExists: true},
		{name: "Use an invalid filepath", theme: "./someinvalidfile.toml", fileExists: false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Successfully return a theme if a file exists
			assert.NotNil(t, styles.SelectTheme(tt.theme))

			// Successfully return a theme if a file doesn't exist
			if !tt.fileExists {
				assert.NotNil(t, styles.SelectTheme(tt.theme))
			}
		})
	}
}


================================================
FILE: styles/theme.json
================================================
{
  "document": {
    "block_prefix": "\n",
    "block_suffix": "\n",
    "color": "252",
    "margin": 2
  },
  "block_quote": {
    "indent": 1,
    "indent_token": "│ "
  },
  "paragraph": {},
  "list": {
    "level_indent": 2
  },
  "heading": {
    "block_suffix": "\n",
    "color": "39",
    "bold": true
  },
  "h1": {
    "prefix": "██ ",
    "suffix": " ",
    "color": "#9fc",
    "bold": true
  },
  "h2": {
    "prefix": "▓▓▓ ",
    "color": "#1cc"
  },
  "h3": {
    "prefix": "▒▒▒▒ ",
    "color": "#29c"
  },
  "h4": {
    "color": "#559",
    "prefix": "░░░░░ "
  },
  "h5": {},
  "h6": {},
  "text": {},
  "strikethrough": {
    "crossed_out": true
  },
  "emph": {
    "italic": true
  },
  "strong": {
    "bold": true
  },
  "hr": {
    "color": "240",
    "format": "\n--------\n"
  },
  "item": {
    "block_prefix": "• "
  },
  "enumeration": {
    "block_prefix": ". "
  },
  "task": {
    "ticked": "[✓] ",
    "unticked": "[ ] "
  },
  "link": {
    "color": "30",
    "underline": true
  },
  "link_text": {
    "color": "35",
    "bold": true
  },
  "image": {
    "color": "212",
    "underline": true
  },
  "image_text": {
    "color": "243",
    "format": "Image: {{.text}} →"
  },
  "code": {
    "prefix": " ",
    "suffix": " ",
    "color": "203",
    "background_color": "236"
  },
  "code_block": {
    "theme": "dracula",
    "margin": 2
  },
  "table": {
    "center_separator": "┼",
    "column_separator": "│",
    "row_separator": "─"
  },
  "definition_list": {},
  "definition_term": {},
  "definition_description": {
    "block_prefix": "\n🠶 "
  },
  "html_block": {},
  "html_span": {}
}
Download .txt
gitextract_svh9gyz7/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── dependabot.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── goreleaser.yml
│       └── test.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── SECURITY.md
├── docs/
│   └── development/
│       └── README.md
├── examples/
│   ├── ascii_slides.md
│   ├── code_blocks.md
│   ├── custom_remote_theme.md
│   ├── custom_theme.md
│   ├── import.md
│   ├── metadata.md
│   ├── preprocess.md
│   ├── slides.md
│   └── theme.json
├── go.mod
├── go.sum
├── internal/
│   ├── cmd/
│   │   └── serve.go
│   ├── code/
│   │   ├── code.go
│   │   ├── code_test.go
│   │   ├── comments.go
│   │   ├── comments_test.go
│   │   ├── execute_test.go
│   │   └── languages.go
│   ├── file/
│   │   ├── file.go
│   │   └── file_test.go
│   ├── meta/
│   │   ├── meta.go
│   │   └── meta_test.go
│   ├── model/
│   │   ├── model.go
│   │   └── tutorial.md
│   ├── navigation/
│   │   ├── navigation.go
│   │   ├── navigation_test.go
│   │   ├── search.go
│   │   └── search_test.go
│   ├── process/
│   │   ├── execute_test.go
│   │   ├── process.go
│   │   └── process_test.go
│   └── server/
│       ├── middleware.go
│       └── server.go
├── main.go
├── snap/
│   └── snapcraft.yaml
└── styles/
    ├── styles.go
    ├── styles_test.go
    └── theme.json
Download .txt
SYMBOL INDEX (109 symbols across 24 files)

FILE: internal/cmd/serve.go
  function init (line 87) | func init() {

FILE: internal/code/code.go
  type Block (line 14) | type Block struct
  type Result (line 20) | type Result struct
  function Parse (line 38) | func Parse(markdown string) ([]Block, error) {
  constant ExitCodeInternalError (line 65) | ExitCodeInternalError = -1
  function Execute (line 69) | func Execute(code Block) Result {

FILE: internal/code/code_test.go
  function TestParse (line 9) | func TestParse(t *testing.T) {

FILE: internal/code/comments.go
  constant comment (line 8) | comment = "///"
  function HideComments (line 13) | func HideComments(content string) string {
  function RemoveComments (line 19) | func RemoveComments(content string) string {

FILE: internal/code/comments_test.go
  function TestHidesComments (line 5) | func TestHidesComments(t *testing.T) {
  function TestNoComments (line 23) | func TestNoComments(t *testing.T) {
  function TestRemoveComments (line 42) | func TestRemoveComments(t *testing.T) {

FILE: internal/code/execute_test.go
  function TestExecute (line 9) | func TestExecute(t *testing.T) {

FILE: internal/code/languages.go
  type cmds (line 5) | type cmds
  type Language (line 9) | type Language struct
  constant Bash (line 19) | Bash       = "bash"
  constant Zsh (line 20) | Zsh        = "zsh"
  constant Fish (line 21) | Fish       = "fish"
  constant Elixir (line 22) | Elixir     = "elixir"
  constant Go (line 23) | Go         = "go"
  constant Javascript (line 24) | Javascript = "javascript"
  constant Lua (line 25) | Lua        = "lua"
  constant OCaml (line 26) | OCaml      = "ocaml"
  constant Perl (line 27) | Perl       = "perl"
  constant Python (line 28) | Python     = "python"
  constant Ruby (line 29) | Ruby       = "ruby"
  constant Rust (line 30) | Rust       = "rust"
  constant Java (line 31) | Java       = "java"
  constant Julia (line 32) | Julia      = "julia"
  constant Cpp (line 33) | Cpp        = "cpp"
  constant Swift (line 34) | Swift      = "swift"
  constant Dart (line 35) | Dart       = "dart"
  constant V (line 36) | V          = "v"
  constant Scala (line 37) | Scala      = "scala"
  constant Haskell (line 38) | Haskell    = "haskell"

FILE: internal/file/file.go
  function Exists (line 13) | func Exists(filepath string) bool {
  function IsExecutable (line 22) | func IsExecutable(s fs.FileInfo) bool {

FILE: internal/file/file_test.go
  function TestExists (line 13) | func TestExists(t *testing.T) {
  function TestIsExecutable (line 33) | func TestIsExecutable(t *testing.T) {

FILE: internal/meta/meta.go
  type parsedMeta (line 17) | type parsedMeta struct
  type Meta (line 26) | type Meta struct
    method Parse (line 44) | func (m *Meta) Parse(header string) (*Meta, bool) {
  function New (line 35) | func New() *Meta {
  function defaultTheme (line 90) | func defaultTheme() string {
  function defaultAuthor (line 98) | func defaultAuthor() string {
  function defaultDate (line 107) | func defaultDate() string {
  function defaultPaging (line 111) | func defaultPaging() string {
  function parseDate (line 115) | func parseDate(value string) string {

FILE: internal/meta/meta_test.go
  function TestMeta_ParseHeader (line 13) | func TestMeta_ParseHeader(t *testing.T) {
  function TestNew (line 176) | func TestNew(t *testing.T) {
  function ExampleMeta_Parse (line 190) | func ExampleMeta_Parse() {

FILE: internal/model/model.go
  constant delimiter (line 33) | delimiter = "\n---\n"
  type Model (line 38) | type Model struct
    method Init (line 60) | func (m Model) Init() tea.Cmd {
    method Load (line 75) | func (m *Model) Load() error {
    method Update (line 113) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    method View (line 206) | func (m Model) View() string {
    method paging (line 232) | func (m *Model) paging() string {
    method CurrentPage (line 299) | func (m *Model) CurrentPage() int {
    method SetPage (line 304) | func (m *Model) SetPage(page int) {
    method Pages (line 314) | func (m *Model) Pages() []string {
  type fileWatchMsg (line 54) | type fileWatchMsg struct
  function fileWatchCmd (line 68) | func fileWatchCmd() tea.Cmd {
  function readFile (line 243) | func readFile(path string) (string, error) {
  function readStdin (line 271) | func readStdin() (string, error) {

FILE: internal/navigation/navigation.go
  type repeatableFunc (line 7) | type repeatableFunc
  type State (line 10) | type State struct
  function Navigate (line 17) | func Navigate(state State, keyPress string) State {
  function bufferIsNumeric (line 73) | func bufferIsNumeric(buffer string) bool {
  function navigateNext (line 78) | func navigateNext(state State) int {
  function navigateSlide (line 88) | func navigateSlide(buffer string, totalSlides int) int {
  function navigatePrevious (line 103) | func navigatePrevious(state State) int {
  function repeatableAction (line 113) | func repeatableAction(fn repeatableFunc, state State) int {

FILE: internal/navigation/navigation_test.go
  function TestNavigation (line 10) | func TestNavigation(t *testing.T) {

FILE: internal/navigation/search.go
  type Model (line 12) | type Model interface
  type Search (line 19) | type Search struct
    method Query (line 38) | func (s *Search) Query() string {
    method SetQuery (line 43) | func (s *Search) SetQuery(query string) {
    method Done (line 50) | func (s *Search) Done() {
    method Begin (line 55) | func (s *Search) Begin() {
    method Execute (line 61) | func (s *Search) Execute(m Model) {
  function NewSearch (line 28) | func NewSearch() Search {

FILE: internal/navigation/search_test.go
  type mockModel (line 7) | type mockModel struct
    method CurrentPage (line 12) | func (m *mockModel) CurrentPage() int {
    method SetPage (line 16) | func (m *mockModel) SetPage(page int) {
    method Pages (line 20) | func (m *mockModel) Pages() []string {
  function TestSearch (line 24) | func TestSearch(t *testing.T) {

FILE: internal/process/execute_test.go
  function TestExecute (line 5) | func TestExecute(t *testing.T) {

FILE: internal/process/process.go
  type Block (line 18) | type Block struct
    method String (line 26) | func (b Block) String() string {
    method Execute (line 51) | func (b *Block) Execute() {
  function Parse (line 35) | func Parse(markdown string) []Block {
  function Pre (line 74) | func Pre(content string) string {

FILE: internal/process/process_test.go
  function TestParse (line 8) | func TestParse(t *testing.T) {

FILE: internal/server/middleware.go
  function slidesMiddleware (line 13) | func slidesMiddleware(srv *Server) wish.Middleware {

FILE: internal/server/server.go
  type Server (line 13) | type Server struct
    method Start (line 42) | func (s *Server) Start() error {
    method Shutdown (line 47) | func (s *Server) Shutdown(ctx context.Context) error {
  function NewServer (line 21) | func NewServer(keyPath, host string, port int, presentation model.Model)...

FILE: main.go
  function init (line 46) | func init() {
  function main (line 53) | func main() {

FILE: styles/styles.go
  constant salmon (line 17) | salmon = lipgloss.Color("#E8B4BC")
  function JoinHorizontal (line 47) | func JoinHorizontal(left, right string, width int) string {
  function JoinVertical (line 53) | func JoinVertical(top, bottom string, height int) string {
  function SelectTheme (line 60) | func SelectTheme(theme string) glamour.TermRendererOption {
  function getDefaultTheme (line 98) | func getDefaultTheme() glamour.TermRendererOption {

FILE: styles/styles_test.go
  function TestSelectTheme (line 12) | func TestSelectTheme(t *testing.T) {
  function TestSelectTheme_file (line 54) | func TestSelectTheme_file(t *testing.T) {
Condensed preview — 53 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (95K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 19,
    "preview": "github: maaslalani\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 834,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 595,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 108,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 43,
    "preview": "Fixes #...\n\n### Changes Introduced\n- \n- \n-\n"
  },
  {
    "path": ".github/workflows/goreleaser.yml",
    "chars": 493,
    "preview": "name: goreleaser\n\non:\n  push:\n    tags:\n      - '*'\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - u"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 355,
    "preview": "name: test\n\non: [ push, pull_request ]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checko"
  },
  {
    "path": ".gitignore",
    "chars": 48,
    "preview": "/slides\n.idea\nslides_ed25519\nslides_ed25519.pub\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 35,
    "preview": "# Code of Conduct\n\nBe nice please!\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 97,
    "preview": "Take a look at the [Development Docs](./docs/development/README.md).\n\nPull requests are welcome!\n"
  },
  {
    "path": "LICENSE",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright (c) 2021 Maas Lalani\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
  },
  {
    "path": "Makefile",
    "chars": 98,
    "preview": "make:\n\tgo run main.go examples/slides.md\n\ntest:\n\tgo test ./... -short\n\nbuild:\n\tgo build -o slides\n"
  },
  {
    "path": "README.md",
    "chars": 7241,
    "preview": "# Slides\n\nSlides in your terminal.\n\n<p align=\"center\">\n  <img src=\"./assets/slides-1.gif?raw=true\" alt=\"Slides Presentat"
  },
  {
    "path": "SECURITY.md",
    "chars": 97,
    "preview": "# Security Policy\n\n## Reporting a Vulnerability\n\nEmail [maas@lalani.dev](mailto:maas@lalani.dev)\n"
  },
  {
    "path": "docs/development/README.md",
    "chars": 811,
    "preview": "# Development\n\nMake changes, and test them by running:\n```\nmake\n```\n\nThis will run `go run main.go examples/slides.md`, "
  },
  {
    "path": "examples/ascii_slides.md",
    "chars": 653,
    "preview": "---\ntheme: ascii\n---\n\n# Welcome to Slides\nA terminal based presentation tool\n\n```go\npackage main\n\nimport \"fmt\"\n\nfunc mai"
  },
  {
    "path": "examples/code_blocks.md",
    "chars": 1996,
    "preview": "# Code blocks\n\nSlides allows you to execute code blocks directly inside your slides!\n\nJust press `ctrl+e` and the result"
  },
  {
    "path": "examples/custom_remote_theme.md",
    "chars": 228,
    "preview": "---\ntheme: https://github.com/maaslalani/slides/raw/main/styles/theme.json\n---\n\n# Slides\n\nThe theme of this slide is fet"
  },
  {
    "path": "examples/custom_theme.md",
    "chars": 112,
    "preview": "---\ntheme: ./examples/theme.json\n---\n\n# Slides\n\nThe above title should be orange and be prefixed with `CUSTOM`.\n"
  },
  {
    "path": "examples/import.md",
    "chars": 82,
    "preview": "This is just an example of how to import text from other files with\npreprocess.md\n"
  },
  {
    "path": "examples/metadata.md",
    "chars": 386,
    "preview": "---\nauthor: Gopher\ndate: May 22, 2022\npaging: Page %d of %d\n---\n\n# Metadata Example\n\nCustomize the bottom information ba"
  },
  {
    "path": "examples/preprocess.md",
    "chars": 1377,
    "preview": "# Slides\n\nYou can add a code block with three tildes (~) and write a command to run before displaying\nthe slides, the te"
  },
  {
    "path": "examples/slides.md",
    "chars": 814,
    "preview": "# Welcome to Slides\nA terminal based presentation tool\n\n```go\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n  fmt.Println(\"W"
  },
  {
    "path": "examples/theme.json",
    "chars": 1640,
    "preview": "{\n  \"document\": {\n    \"block_prefix\": \"\\n\",\n    \"block_suffix\": \"\\n\",\n    \"color\": \"252\",\n    \"margin\": 2\n  },\n  \"block_"
  },
  {
    "path": "go.mod",
    "chars": 2422,
    "preview": "module github.com/maaslalani/slides\n\ngo 1.22\n\nrequire (\n\tgithub.com/atotto/clipboard v0.1.4\n\tgithub.com/charmbracelet/bu"
  },
  {
    "path": "go.sum",
    "chars": 10218,
    "preview": "github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=\ngithub.com/alecthomas/assert/v2 v"
  },
  {
    "path": "internal/cmd/serve.go",
    "chars": 2035,
    "preview": "package cmd\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strconv\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/maaslalani/slides"
  },
  {
    "path": "internal/code/code.go",
    "chars": 3315,
    "preview": "package code\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Block represents a"
  },
  {
    "path": "internal/code/code_test.go",
    "chars": 2033,
    "preview": "package code_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/maaslalani/slides/internal/code\"\n)\n\nfunc TestParse(t *testing.T) {\n"
  },
  {
    "path": "internal/code/comments.go",
    "chars": 545,
    "preview": "package code\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n)\n\nconst comment = \"///\"\n\nvar commentRegexp = regexp.MustCompile(\"(?m)[\\r\\n]"
  },
  {
    "path": "internal/code/comments_test.go",
    "chars": 1071,
    "preview": "package code\n\nimport \"testing\"\n\nfunc TestHidesComments(t *testing.T) {\n\tcontent := `\n///package main\n///\n///import \"fmt\""
  },
  {
    "path": "internal/code/execute_test.go",
    "chars": 1326,
    "preview": "package code_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/maaslalani/slides/internal/code\"\n)\n\nfunc TestExecute(t *testing.T) "
  },
  {
    "path": "internal/code/languages.go",
    "chars": 2767,
    "preview": "package code\n\n// cmds: Multiple commands; placeholders can be used\n// Placeholders <file>, <name> and <path> can be used"
  },
  {
    "path": "internal/file/file.go",
    "chars": 485,
    "preview": "// Package file includes utility functions\n// for working with the filesystem\npackage file\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n)\n\n/"
  },
  {
    "path": "internal/file/file_test.go",
    "chars": 1417,
    "preview": "package file_test\n\nimport (\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/maaslalani/slides/internal/file\"\n\t\"github.com"
  },
  {
    "path": "internal/meta/meta.go",
    "chars": 2514,
    "preview": "// Package meta implements markdown frontmatter parsing for simple\n// slides configuration\npackage meta\n\nimport (\n\t\"os\"\n"
  },
  {
    "path": "internal/meta/meta_test.go",
    "chars": 4606,
    "preview": "package meta_test\n\nimport (\n\t\"fmt\"\n\t\"os/user\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/maaslalani/slides/internal/meta\"\n\t\"github"
  },
  {
    "path": "internal/model/model.go",
    "chars": 7188,
    "preview": "package model\n\nimport (\n\t\"bufio\"\n\t_ \"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/atotto/clipboa"
  },
  {
    "path": "internal/model/tutorial.md",
    "chars": 911,
    "preview": "# Welcome to Slides\nA terminal based presentation tool\n\n## Everything is markdown\nIn fact this entire presentation is a "
  },
  {
    "path": "internal/navigation/navigation.go",
    "chars": 2717,
    "preview": "package navigation\n\nimport (\n\t\"strconv\"\n)\n\ntype repeatableFunc func(slide, totalSlides int) int\n\n// State tracks the cur"
  },
  {
    "path": "internal/navigation/navigation_test.go",
    "chars": 963,
    "preview": "package navigation\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNavigation(t *test"
  },
  {
    "path": "internal/navigation/search.go",
    "chars": 2052,
    "preview": "package navigation\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\t\"github.com/maaslalani"
  },
  {
    "path": "internal/navigation/search_test.go",
    "chars": 1264,
    "preview": "package navigation\n\nimport (\n\t\"testing\"\n)\n\ntype mockModel struct {\n\tslides []string\n\tpage   int\n}\n\nfunc (m *mockModel) C"
  },
  {
    "path": "internal/process/execute_test.go",
    "chars": 603,
    "preview": "package process\n\nimport \"testing\"\n\nfunc TestExecute(t *testing.T) {\n\ttt := []struct {\n\t\tblock Block\n\t\twant  string\n\t}{\n\t"
  },
  {
    "path": "internal/process/process.go",
    "chars": 2190,
    "preview": "package process\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// Block represents a pre-processable block wh"
  },
  {
    "path": "internal/process/process_test.go",
    "chars": 962,
    "preview": "package process\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestParse(t *testing.T) {\n\tmd := `\n# Slide\n\n~~~sd Replace Proces"
  },
  {
    "path": "internal/server/middleware.go",
    "chars": 799,
    "preview": "package server\n\nimport (\n\t\"fmt\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/ssh\"\n\t\"github.com/"
  },
  {
    "path": "internal/server/server.go",
    "chars": 1007,
    "preview": "package server\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/charmbracelet/ssh\"\n\t\"github.com/charmbracelet/wish\"\n\t\"github.co"
  },
  {
    "path": "main.go",
    "chars": 1069,
    "preview": "package main\n\nimport (\n\t_ \"embed\"\n\t\"os\"\n\t\"time\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/maaslalani/slide"
  },
  {
    "path": "snap/snapcraft.yaml",
    "chars": 991,
    "preview": "name: slides\nadopt-info: slides\nsummary: Slides in your terminal.\ndescription: |\n  Slides in your terminal.\n  \n  Usage:\n"
  },
  {
    "path": "styles/styles.go",
    "chars": 3188,
    "preview": "// Package styles implements the theming logic for slides\npackage styles\n\nimport (\n\t_ \"embed\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\""
  },
  {
    "path": "styles/styles_test.go",
    "chars": 2257,
    "preview": "package styles_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/charmbracelet/glamour\"\n\t\"github.com/charmbracelet/glamour/ansi\"\n\t"
  },
  {
    "path": "styles/theme.json",
    "chars": 1636,
    "preview": "{\n  \"document\": {\n    \"block_prefix\": \"\\n\",\n    \"block_suffix\": \"\\n\",\n    \"color\": \"252\",\n    \"margin\": 2\n  },\n  \"block_"
  }
]

About this extraction

This page contains the full source code of the maaslalani/slides GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 53 files (81.8 KB), approximately 28.4k tokens, and a symbol index with 109 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!