[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: maaslalani\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "Fixes #...\n\n### Changes Introduced\n- \n- \n-\n"
  },
  {
    "path": ".github/workflows/goreleaser.yml",
    "content": "name: goreleaser\n\non:\n  push:\n    tags:\n      - '*'\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - name: Set up Go\n        uses: actions/setup-go@v2\n        with:\n          go-version: 1.17\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@v2\n        with:\n          distribution: goreleaser\n          version: latest\n          args: release --rm-dist\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: test\n\non: [ push, pull_request ]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - name: Set up Go\n        uses: actions/setup-go@v2\n        with:\n          go-version: 1.17\n\n      - name: Lint\n        uses: golangci/golangci-lint-action@v2\n\n      - name: Test\n        run: go test -race -v -short ./...\n"
  },
  {
    "path": ".gitignore",
    "content": "/slides\n.idea\nslides_ed25519\nslides_ed25519.pub\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Code of Conduct\n\nBe nice please!\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "Take a look at the [Development Docs](./docs/development/README.md).\n\nPull requests are welcome!\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Maas Lalani\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "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",
    "content": "# Slides\n\nSlides in your terminal.\n\n<p align=\"center\">\n  <img src=\"./assets/slides-1.gif?raw=true\" alt=\"Slides Presentation\" />\n</p>\n\n### Installation\n[![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)\n[![Snapcraft](https://snapcraft.io/slides/badge.svg)](https://snapcraft.io/slides)\n[![AUR](https://img.shields.io/aur/version/slides?label=AUR)](https://aur.archlinux.org/packages/slides)\n\n<details markdown=\"block\">\n<summary>Instructions</summary>\n\n#### MacOS\n```\nbrew install slides\n```\n#### Arch\n```\nyay -S slides\n```\n#### Nixpkgs (unstable)\n```\nnix-env -iA nixpkgs.slides\n```\n#### Any Linux Distro running `snapd`\n```\nsudo snap install slides\n```\n#### Go\n```\ngo install github.com/maaslalani/slides@latest\n```\nFrom source:\n```\ngit clone https://github.com/maaslalani/slides.git\ncd slides\ngo install\n```\n\nYou can also download a binary from the [releases](https://github.com/maaslalani/slides/releases) page.\n\n</details>\n\n\n### Usage\nCreate a simple markdown file that contains your slides:\n\n````markdown\n# Welcome to Slides\nA terminal based presentation tool\n\n---\n\n## Everything is markdown\nIn fact, this entire presentation is a markdown file.\n\n---\n\n## Everything happens in your terminal\nCreate slides and present them without ever leaving your terminal.\n\n---\n\n## Code execution\n```go\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n  fmt.Println(\"Execute code directly inside the slides\")\n}\n```\n\nYou can execute code inside your slides by pressing `<C-e>`,\nthe output of your command will be displayed at the end of the current slide.\n\n---\n\n## Pre-process slides\n\nYou can add a code block with three tildes (`~`) and write a command to run *before* displaying\nthe slides, the text inside the code block will be passed as `stdin` to the command\nand the code block will be replaced with the `stdout` of the command.\n\n```\n~~~graph-easy --as=boxart\n[ A ] - to -> [ B ]\n~~~\n```\n\nThe above will be pre-processed to look like:\n\n┌───┐  to   ┌───┐\n│ A │ ────> │ B │\n└───┘       └───┘\n\nFor security reasons, you must pass a file that has execution permissions\nfor the slides to be pre-processed. You can use `chmod` to add these permissions.\n\n```bash\nchmod +x file.md\n```\n\n````\n\nCheckout the [example slides](https://github.com/maaslalani/slides/tree/main/examples).\n\nThen, to present, run:\n```\nslides presentation.md\n```\n\nIf given a file name, `slides` will automatically look for changes in the file and update the presentation live.\n\n`slides` also accepts input through `stdin`:\n```\ncurl http://example.com/slides.md | slides\n```\n\nGo to the first slide with the following key sequence:\n* <kbd>g</kbd> <kbd>g</kbd>\n\nGo to the next slide with any of the following key sequences:\n* <kbd>space</kbd>\n* <kbd>right</kbd>\n* <kbd>down</kbd>\n* <kbd>enter</kbd>\n* <kbd>n</kbd>\n* <kbd>j</kbd>\n* <kbd>l</kbd>\n* <kbd>Page Down</kbd>\n* number + any of the above (go forward n slides)\n\nGo to the previous slide with any of the following key sequences:\n* <kbd>left</kbd>\n* <kbd>up</kbd>\n* <kbd>p</kbd>\n* <kbd>h</kbd>\n* <kbd>k</kbd>\n* <kbd>N</kbd>\n* <kbd>Page Up</kbd>\n* number + any of the above (go back n slides)\n\nGo to a specific slide with the following key sequence:\n\n* number + <kbd>G</kbd>\n\nGo to the last slide with the following key:\n\n* <kbd>G</kbd>\n\n### Search\n\nTo quickly jump to the right slide, you can use the search function.\n\nPress <kbd>/</kbd>, enter your search term and press <kbd>Enter</kbd>  \n(*The search term is interpreted as a regular expression. The `/i` flag causes case-insensitivity.*).\n\nPress <kbd>ctrl+n</kbd> after a search to go to the next search result.\n\n### Code Execution\n\nIf slides finds a code block on the current slides it can execute the code block and display the result as virtual text\non the screen.\n\nPress <kbd>ctrl+e</kbd> on a slide with a code block to execute it and display the result.\n\n### Pre-processing\n\nYou can add a code block with three tildes (`~`) and write a command to run\n*before* displaying the slides, the text inside the code block will be passed\nas `stdin` to the command and the code block will be replaced with the `stdout`\nof the command. Wrap the pre-processed block in three backticks to keep\nproper formatting and new lines.\n\n````\n```\n~~~graph-easy --as=boxart\n[ A ] - to -> [ B ]\n~~~\n```\n````\n\nThe above will be pre-processed to look like:\n\n```\n┌───┐  to   ┌───┐\n│ A │ ────> │ B │\n└───┘       └───┘\n```\n\nFor security reasons, you must pass a file that has execution permissions\nfor the slides to be pre-processed. You can use `chmod` to add these permissions.\n\n```bash\nchmod +x file.md\n```\n\n### Configuration\n\n`slides` allows you to customize your presentation's look and feel with metadata at the top of your `slides.md`.\n\n> This section is entirely optional, `slides` will use sensible defaults if this section or any field in the section is omitted.\n\n```yaml\n---\ntheme: ./path/to/theme.json\nauthor: Gopher\ndate: MMMM dd, YYYY\npaging: Slide %d / %d\n---\n```\n\n* `theme`: Path to `json` file containing a [glamour\n  theme](https://github.com/charmbracelet/glamour/tree/master/styles), can also\n  be a link to a remote `json` file which slides will fetch before presenting.\n* `author`: A `string` to display on the bottom-left corner of the presentation\n  view. Defaults to the OS current user's full name. Can be empty to hide the author.\n* `date`: A `string` that is used to format today's date in the `YYYY-MM-DD` format. If the date is not a valid\n  format, the string will be displayed. Defaults to `YYYY-MM-DD`.\n* `paging`: A `string` that contains 0 or more `%d` directives. The first `%d`\n  will be replaced with the current slide number and the second `%d` will be\n  replaced with the total slides count. Defaults to `Slide %d / %d`.\n  You will need to surround the paging value with quotes if it starts with `%`.\n\n#### Date format\n\nGiven the date _January 02, 2006_:\n\n| Value  | Translates to |\n|--------|---------------|\n| `YYYY` | 2006          |\n| `YY`   | 06            |\n| `MMMM` | January       |\n| `MMM`  | Jan           |\n| `MM`   | 01            |\n| `mm`   | 1             |\n| `DD`   | 02            |\n| `dd`   | 2             |\n\n### SSH\n\nSlides is accessible over `ssh` if hosted on a machine through the `slides\nserve [file]` command.\n\nOn a machine, run:\n\n```\nslides serve [file]\n```\n\nThen, on another machine (or same machine), `ssh` into the port specified by\nthe `slides serve [file]` command:\n```\nssh 127.0.0.1 -p 53531\n```\n\nYou will be able to access the presentation hosted over SSH! You can use this\nto present with `slides` from a computer that doesn't have `slides` installed,\nbut does have `ssh`. Or, let your viewers have access to the slides on their\nown computer without needing to download `slides` and the presentation file.\n\n### Alternatives\n\n**Credits**: This project was heavily inspired by [`lookatme`](https://github.com/d0c-s4vage/lookatme).\n\n* [`lookatme`](https://github.com/d0c-s4vage/lookatme)\n* [`sli.dev`](https://sli.dev/)\n* [`sent`](https://tools.suckless.org/sent/)\n* [`presenterm`](https://github.com/mfontanini/presenterm)\n\n### Development\nSee the [development documentation](./docs/development)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Reporting a Vulnerability\n\nEmail [maas@lalani.dev](mailto:maas@lalani.dev)\n"
  },
  {
    "path": "docs/development/README.md",
    "content": "# Development\n\nMake changes, and test them by running:\n```\nmake\n```\n\nThis will run `go run main.go examples/slides.md`, you can then ensure\neverything still works.\n\nIf you're adding a feature that requires a specific piece of markdown, you can\nadd a file with your test case into `examples/<test>.md` and iterate on that file.\n\nEnsure tests are still passing\n```\nmake test\n```\n\n### Breaking Changes\nMost changes should be entirely backwards compatible.\nEnsure that `slides examples/slides.md` still works.\n\n### Codebase\nInitialization (command-line interface, defaults) happens in [`cmd/root.go`](../../cmd/root.go).\nInteraction (controls, input, output) happens in [`model.go`](../../internal/model/model.go)\nOptional configuration (e.g. `theme: dark`) can be added to [`meta.go`](../../internal/meta/meta.go)\n"
  },
  {
    "path": "examples/ascii_slides.md",
    "content": "---\ntheme: ascii\n---\n\n# Welcome to Slides\nA terminal based presentation tool\n\n```go\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n  fmt.Println(\"Written in Go!\")\n}\n```\n\n---\n\n## Everything is markdown\nIn fact this entire presentation is a markdown file\n\n---\n\n# h1\n## h2\n### h3\n#### h4\n##### h5\n###### h6\n\n# Markdown components\nYou can use everything in markdown!\n* Like bulleted list\n* You know the deal\n\n1. Numbered lists too\n\n| Tables | Too    |\n| ------ | ------ |\n| Even   | Tables |\n\n---\n\nAll you need to do is separate slides with triple dashes `---` on a separate line,\nlike so:\n\n```markdown\n# Slide 1\nSome stuff\n\n--- \n\n# Slide 2\nSome other stuff\n```\n"
  },
  {
    "path": "examples/code_blocks.md",
    "content": "# Code blocks\n\nSlides allows you to execute code blocks directly inside your slides!\n\nJust press `ctrl+e` and the result of the code block will be displayed as virtual text in your slides.\n\nCurrently supported languages:\n\n<!-- Use comments in your markdown! -->\n\n* `bash`\n* `zsh`\n* `fish`\n* `elixir`\n* `go`\n* `javascript`\n* `python`\n* `ruby`\n* `perl`\n* `rust`\n* `java`\n* `cpp`\n* `swift`\n* `dart`\n* `v`\n<!-- * `secret` -->\n\n---\n\n### Bash\n\n```bash\nls\n```\n\n---\n\n### Zsh\n\n```zsh\nls\n```\n\n---\n\n### Fish\n\n```fish\nls\n```\n\n---\n\n### Elixir\n\n```elixir\nIO.puts \"Hello, world!\"\n```\n\n---\n\n### Go\n\nUse `///` to hide verbose code but still allow the ability to execute it.\n\nIf you press `y` to copy (yank) this code block it will return the full snippet.\n\nAnd, if you press `ctrl+e` it will run the program without error, even though\nwhat is being displayed is not a valid go program because we have commented out\nsome boilerplate to focus on the important parts.\n\n```go\n///package main\n///\nimport \"fmt\"\n///\n///func main() {\nfmt.Println(\"Hello, world!\")\n///}\n```\n\n---\n\n### Javascript\n\n```javascript\nconsole.log(\"Hello, world!\")\n```\n\n---\n\n### Lua\n\n```lua\nprint(\"Hello, World!\")\n```\n\n---\n\n### Python\n\n```python\nprint(\"Hello, world!\")\n```\n\n---\n\n### Ruby\n\n```ruby\nputs \"Hello, world!\"\n```\n\n---\n\n### Perl\n\n```perl\nprint (\"hello, world\");\n```\n\n---\n\n### Rust\n\n```rust\nfn main() {\n    println!(\"Hello, world!\");\n}\n```\n\n---\n\n### Java\n```java\npublic class Main {\n    public static void main(String[] args) {\n        System.out.println(\"Hello, world!\");\n    }\n}\n```\n\n---\n\n### Julia\n```julia\nprintln(\"Hello, world!\")\n```\n\n---\n\n### C++\n```cpp\n#include <iostream>\n\nint main() {\n    std::cout << \"Hello, world!\" << std::endl;\n    return 0;\n}\n```\n\n---\n\n### Swift\n```swift\nprint(\"Hello, world!\")\n```\n\n---\n\n### Dart\n```dart\nvoid main() {\n  print(\"Hello, world!\");\n}\n```\n\n### V\n\n```v\nprintln('Hello, world!')\n```\n\n---\n\n### Scala\n\n```scala\n//> using dep com.lihaoyi::pprint:0.8.1\n\nobject Main extends App {\n  println(\"Hello\")\n}\n```\n"
  },
  {
    "path": "examples/custom_remote_theme.md",
    "content": "---\ntheme: https://github.com/maaslalani/slides/raw/main/styles/theme.json\n---\n\n# Slides\n\nThe theme of this slide is fetched from https://github.com/maaslalani/slides/raw/main/styles/theme.json, the title above should be green.\n"
  },
  {
    "path": "examples/custom_theme.md",
    "content": "---\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",
    "content": "This is just an example of how to import text from other files with\npreprocess.md\n"
  },
  {
    "path": "examples/metadata.md",
    "content": "---\nauthor: Gopher\ndate: May 22, 2022\npaging: Page %d of %d\n---\n\n# Metadata Example\n\nCustomize the bottom information bar by adding metadata to your `slides.md` file.\n\n```\n--- \nauthor: Gopher\ndate: May 22, 2022\npaging: Page %d of %d\n--- \n```\n\n---\n\n# Metadata Example\n\nYou can also hide the bottom bar by leaving all of the fields blank\n\n```\n--- \nauthor: \"\"\ndate: \"\"\npaging: \"\"\n--- \n```\n"
  },
  {
    "path": "examples/preprocess.md",
    "content": "# Slides\n\nYou can add a code block with three tildes (~) and write a command to run before displaying\nthe slides, the text inside the code block will be passed as stdin to the command\nand the code block will be replaced with the stdout of the command.\n\n```\n~~~graph-easy --as=boxart\n[ A ] - to -> [ B ]\n~~~\n```\n\nThe above will be pre-processed to look like:\n\nNOTE: You need `graph-easy` installed and in your `$PATH`\n\n```\n┌───┐  to   ┌───┐\n│ A │ ────> │ B │\n└───┘       └───┘\n```\n\nFor security reasons, you must pass a file that has execution permissions\nfor the slides to be pre-processed.\n\n```\nchmod +x file.md\n```\n\n---\n\n~~~sd replaced processed\nThis content will be passed in as stdin and will be replaced.\n~~~\n\n---\n\n\nAny command will work\n\n~~~echo \"You can do whatever, really\"\nThis doesn't matter, since it will be replaced by the stdout\nof the command above because the command will disregard stdin.\n~~~\n---\n\n\nYou can use this to import snippets of code from other files:\n\n~~~xargs cat\nexamples/import.md\n~~~\n\n---\n\n\n## More pre-process examples:\n\n### PlantUML\n\n```\n~~~plantuml -utxt -pipe\n@startuml\nA --> B: to\n@enduml\n~~~\n```\n\nThe above will be pre-processed to look like:\n\nNOTE: You need `plantuml` installed and in your `$PATH`\n\n```\n┌─┐          ┌─┐\n│A│          │B│\n└┬┘          └┬┘\n │    to      │\n │ ─ ─ ─ ─ ─ >│\n┌┴┐          ┌┴┐\n│A│          │B│\n└─┘          └─┘\n\n"
  },
  {
    "path": "examples/slides.md",
    "content": "# Welcome to Slides\nA terminal based presentation tool\n\n```go\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n  fmt.Println(\"Written in Go!\")\n}\n```\n\n---\n\n## Everything is markdown\nIn fact this entire presentation is a markdown file\n\n---\n\n# h1\n## h2\n### h3\n#### h4\n##### h5\n###### h6\n\n---\n\n# Markdown components\nYou can use everything in markdown!\n* Like bulleted list\n* You know the deal\n\n1. Numbered lists too\n\n---\n\n# Tables\n\n| Tables | Too    |\n| ------ | ------ |\n| Even   | Tables |\n\n---\n\n# Graphs\n\n```\ndigraph {\n    rankdir = LR;\n    a -> b;\n    b -> c;\n}\n```\n```\n┌───┐     ┌───┐     ┌───┐\n│ a │ ──▶ │ b │ ──▶ │ c │\n└───┘     └───┘     └───┘\n```\n---\n\nAll you need to do is separate slides with triple dashes\n`---` on a separate line, like so:\n\n```markdown\n# Slide 1\nSome stuff\n\n--- \n\n# Slide 2\nSome other stuff\n```\n"
  },
  {
    "path": "examples/theme.json",
    "content": "{\n  \"document\": {\n    \"block_prefix\": \"\\n\",\n    \"block_suffix\": \"\\n\",\n    \"color\": \"252\",\n    \"margin\": 2\n  },\n  \"block_quote\": {\n    \"indent\": 1,\n    \"indent_token\": \"│ \"\n  },\n  \"paragraph\": {},\n  \"list\": {\n    \"level_indent\": 2\n  },\n  \"heading\": {\n    \"block_suffix\": \"\\n\",\n    \"color\": \"39\",\n    \"bold\": true\n  },\n  \"h1\": {\n    \"prefix\": \"CUSTOM \",\n    \"suffix\": \" \",\n    \"color\": \"#fa0\",\n    \"bold\": true\n  },\n  \"h2\": {\n    \"prefix\": \"▓▓▓ \",\n    \"color\": \"#1cc\"\n  },\n  \"h3\": {\n    \"prefix\": \"▒▒▒▒ \",\n    \"color\": \"#29c\"\n  },\n  \"h4\": {\n    \"color\": \"#559\",\n    \"prefix\": \"░░░░░ \"\n  },\n  \"h5\": {},\n  \"h6\": {},\n  \"text\": {},\n  \"strikethrough\": {\n    \"crossed_out\": true\n  },\n  \"emph\": {\n    \"italic\": true\n  },\n  \"strong\": {\n    \"bold\": true\n  },\n  \"hr\": {\n    \"color\": \"240\",\n    \"format\": \"\\n--------\\n\"\n  },\n  \"item\": {\n    \"block_prefix\": \"• \"\n  },\n  \"enumeration\": {\n    \"block_prefix\": \". \"\n  },\n  \"task\": {\n    \"ticked\": \"[✓] \",\n    \"unticked\": \"[ ] \"\n  },\n  \"link\": {\n    \"color\": \"30\",\n    \"underline\": true\n  },\n  \"link_text\": {\n    \"color\": \"35\",\n    \"bold\": true\n  },\n  \"image\": {\n    \"color\": \"212\",\n    \"underline\": true\n  },\n  \"image_text\": {\n    \"color\": \"243\",\n    \"format\": \"Image: {{.text}} →\"\n  },\n  \"code\": {\n    \"prefix\": \" \",\n    \"suffix\": \" \",\n    \"color\": \"203\",\n    \"background_color\": \"236\"\n  },\n  \"code_block\": {\n    \"theme\": \"dracula\",\n    \"margin\": 2\n  },\n  \"table\": {\n    \"center_separator\": \"┼\",\n    \"column_separator\": \"│\",\n    \"row_separator\": \"─\"\n  },\n  \"definition_list\": {},\n  \"definition_term\": {},\n  \"definition_description\": {\n    \"block_prefix\": \"\\n🠶 \"\n  },\n  \"html_block\": {},\n  \"html_span\": {}\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/maaslalani/slides\n\ngo 1.22\n\nrequire (\n\tgithub.com/atotto/clipboard v0.1.4\n\tgithub.com/charmbracelet/bubbles v0.18.0\n\tgithub.com/charmbracelet/bubbletea v0.26.2\n\tgithub.com/charmbracelet/glamour v0.7.0\n\tgithub.com/charmbracelet/lipgloss v0.10.0\n\tgithub.com/charmbracelet/ssh v0.0.0-20240507011153-ec70bd03034c\n\tgithub.com/charmbracelet/wish v1.4.0\n\tgithub.com/muesli/coral v1.0.0\n\tgithub.com/muesli/termenv v0.15.2\n\tgithub.com/stretchr/testify v1.9.0\n\tgopkg.in/yaml.v2 v2.4.0\n)\n\nrequire (\n\tgithub.com/alecthomas/chroma/v2 v2.8.0 // indirect\n\tgithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/aymerick/douceur v0.2.0 // indirect\n\tgithub.com/charmbracelet/keygen v0.5.0 // indirect\n\tgithub.com/charmbracelet/log v0.4.0 // indirect\n\tgithub.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 // indirect\n\tgithub.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd // indirect\n\tgithub.com/creack/pty v1.1.21 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dlclark/regexp2 v1.4.0 // indirect\n\tgithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect\n\tgithub.com/go-logfmt/logfmt v0.6.0 // indirect\n\tgithub.com/gorilla/css v1.0.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.0.0 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.2.0 // indirect\n\tgithub.com/mattn/go-isatty v0.0.18 // indirect\n\tgithub.com/mattn/go-localereader v0.0.1 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.15 // indirect\n\tgithub.com/microcosm-cc/bluemonday v1.0.25 // indirect\n\tgithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/muesli/reflow v0.3.0 // indirect\n\tgithub.com/olekukonko/tablewriter v0.0.5 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/yuin/goldmark v1.5.4 // indirect\n\tgithub.com/yuin/goldmark-emoji v1.0.2 // indirect\n\tgolang.org/x/crypto v0.21.0 // indirect\n\tgolang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect\n\tgolang.org/x/net v0.22.0 // indirect\n\tgolang.org/x/sync v0.7.0 // indirect\n\tgolang.org/x/sys v0.20.0 // indirect\n\tgolang.org/x/term v0.20.0 // indirect\n\tgolang.org/x/text v0.14.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=\ngithub.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=\ngithub.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264=\ngithub.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=\ngithub.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=\ngithub.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=\ngithub.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=\ngithub.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=\ngithub.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=\ngithub.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=\ngithub.com/charmbracelet/bubbletea v0.26.2 h1:Eeb+n75Om9gQ+I6YpbCXQRKHt5Pn4vMwusQpwLiEgJQ=\ngithub.com/charmbracelet/bubbletea v0.26.2/go.mod h1:6I0nZ3YHUrQj7YHIHlM8RySX4ZIthTliMY+W8X8b+Gs=\ngithub.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng=\ngithub.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps=\ngithub.com/charmbracelet/keygen v0.5.0 h1:XY0fsoYiCSM9axkrU+2ziE6u6YjJulo/b9Dghnw6MZc=\ngithub.com/charmbracelet/keygen v0.5.0/go.mod h1:DfvCgLHxZ9rJxdK0DGw3C/LkV4SgdGbnliHcObV3L+8=\ngithub.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=\ngithub.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=\ngithub.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=\ngithub.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=\ngithub.com/charmbracelet/ssh v0.0.0-20240507011153-ec70bd03034c h1:nsxEhgGnHTGPh5qXr7EBHOKaaJ1nmQWIcI5TLRPYDqo=\ngithub.com/charmbracelet/ssh v0.0.0-20240507011153-ec70bd03034c/go.mod h1:8/Ve8iGRRIGFM1kepYfRF2pEOF5Y3TEZYoJaA54228U=\ngithub.com/charmbracelet/wish v1.4.0 h1:pL1uVP/YuYgJheHEj98teZ/n6pMYnmlZq/fcHvomrfc=\ngithub.com/charmbracelet/wish v1.4.0/go.mod h1:ew4/MjJVfW/akEO9KmrQHQv1F7bQRGscRMrA+KtovTk=\ngithub.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 h1:3RXpZWGWTOeVXCTv0Dnzxdv/MhNUkBfEcbaTY0zrTQI=\ngithub.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=\ngithub.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd h1:HqBjkSFXXfW4IgX3TMKipWoPEN08T3Pi4SA/3DLss/U=\ngithub.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd/go.mod h1:6GZ13FjIP6eOCqWU4lqgveGnYxQo9c3qBzHPeFu4HBE=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=\ngithub.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=\ngithub.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=\ngithub.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=\ngithub.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=\ngithub.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=\ngithub.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=\ngithub.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=\ngithub.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=\ngithub.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=\ngithub.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=\ngithub.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=\ngithub.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=\ngithub.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=\ngithub.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=\ngithub.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/muesli/coral v1.0.0 h1:odyqkoEg4aJAINOzvnjN4tUsdp+Zleccs7tRIAkkYzU=\ngithub.com/muesli/coral v1.0.0/go.mod h1:bf91M/dkp7iHQw73HOoR9PekdTJMTD6ihJgWoDitde8=\ngithub.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=\ngithub.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=\ngithub.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=\ngithub.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=\ngithub.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=\ngithub.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=\ngithub.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=\ngithub.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=\ngolang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=\ngolang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=\ngolang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=\ngolang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=\ngolang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=\ngolang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=\ngolang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=\ngolang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=\ngolang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=\ngolang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "internal/cmd/serve.go",
    "content": "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/internal/model\"\n\t\"github.com/maaslalani/slides/internal/navigation\"\n\t\"github.com/maaslalani/slides/internal/server\"\n\t\"github.com/muesli/coral\"\n)\n\nvar (\n\thost     string\n\tport     int\n\tkeyPath  string\n\terr      error\n\tfileName string\n)\n\n// ServeCmd is the command for serving the presentation. It starts the slides\n// server allowing for connections.\nvar ServeCmd = &coral.Command{\n\tUse:     \"serve <file.md>\",\n\tAliases: []string{\"server\"},\n\tShort:   \"Start an SSH server to run slides\",\n\tArgs:    coral.ArbitraryArgs,\n\tRunE: func(cmd *coral.Command, args []string) error {\n\t\tk := os.Getenv(\"SLIDES_SERVER_KEY_PATH\")\n\t\tif k != \"\" {\n\t\t\tkeyPath = k\n\t\t}\n\t\th := os.Getenv(\"SLIDES_SERVER_HOST\")\n\t\tif h != \"\" {\n\t\t\thost = h\n\t\t}\n\t\tp := os.Getenv(\"SLIDES_SERVER_PORT\")\n\t\tif p != \"\" {\n\t\t\tport, _ = strconv.Atoi(p)\n\t\t}\n\n\t\tif len(args) > 0 {\n\t\t\tfileName = args[0]\n\t\t}\n\n\t\tpresentation := model.Model{\n\t\t\tPage:     0,\n\t\t\tDate:     time.Now().Format(\"2006-01-02\"),\n\t\t\tFileName: fileName,\n\t\t\tSearch:   navigation.NewSearch(),\n\t\t}\n\t\terr = presentation.Load()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ts, err := server.NewServer(keyPath, host, port, presentation)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdone := make(chan os.Signal, 1)\n\t\tsignal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)\n\t\tlog.Printf(\"Starting Slides server on %s:%d\", host, port)\n\t\tgo func() {\n\t\t\tif err = s.Start(); err != nil {\n\t\t\t\tlog.Fatalln(err)\n\t\t\t}\n\t\t}()\n\n\t\t<-done\n\t\tlog.Print(\"Stopping Slides server\")\n\t\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\t\tdefer func() { cancel() }()\n\t\tif err := s.Shutdown(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn err\n\t},\n}\n\nfunc init() {\n\tServeCmd.Flags().StringVar(&keyPath, \"keyPath\", \"slides\", \"Server private key path\")\n\tServeCmd.Flags().StringVar(&host, \"host\", \"localhost\", \"Server host to bind to\")\n\tServeCmd.Flags().IntVar(&port, \"port\", 53531, \"Server port to bind to\")\n}\n"
  },
  {
    "path": "internal/code/code.go",
    "content": "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 code block.\ntype Block struct {\n\tCode     string\n\tLanguage string\n}\n\n// Result represents the output for an executed code block.\ntype Result struct {\n\tOut           string\n\tExitCode      int\n\tExecutionTime time.Duration\n}\n\n// ?: means non-capture group\nvar re = regexp.MustCompile(\"(?s)(?:```|~~~)(\\\\w+)\\n(.*?)\\n(?:```|~~~)\\\\s?\")\n\nvar (\n\t// ErrParse is the returned error when we cannot parse the code block (i.e.\n\t// there is no code block on the current slide) or the code block is\n\t// incorrectly written.\n\tErrParse = errors.New(\"Error: could not parse code block\")\n)\n\n// Parse takes a block of markdown and returns an array of Block's with code\n// and associated languages\nfunc Parse(markdown string) ([]Block, error) {\n\tmatches := re.FindAllStringSubmatch(markdown, -1)\n\n\tvar rv []Block\n\tfor _, match := range matches {\n\t\t// There was either no language specified or no code block\n\t\t// Either way, we cannot execute the expression\n\t\tif len(match) < 3 {\n\t\t\tcontinue\n\t\t}\n\t\trv = append(rv, Block{\n\t\t\tLanguage: match[1],\n\t\t\tCode:     RemoveComments(match[2]),\n\t\t})\n\n\t}\n\n\tif len(rv) == 0 {\n\t\treturn nil, ErrParse\n\t}\n\n\treturn rv, nil\n}\n\nconst (\n\t// ExitCodeInternalError represents the exit code in which the code\n\t// executing the code didn't work.\n\tExitCodeInternalError = -1\n)\n\n// Execute takes a code.Block and returns the output of the executed code\nfunc Execute(code Block) Result {\n\t// Check supported language\n\tlanguage, ok := Languages[code.Language]\n\tif !ok {\n\t\treturn Result{\n\t\t\tOut:      \"Error: unsupported language\",\n\t\t\tExitCode: ExitCodeInternalError,\n\t\t}\n\t}\n\n\t// Write the code block to a temporary file\n\tf, err := os.CreateTemp(os.TempDir(), \"slides-*.\"+Languages[code.Language].Extension)\n\tif err != nil {\n\t\treturn Result{\n\t\t\tOut:      \"Error: could not create file\",\n\t\t\tExitCode: ExitCodeInternalError,\n\t\t}\n\t}\n\n\tdefer f.Close()\n\tdefer os.Remove(f.Name())\n\n\t_, err = f.WriteString(code.Code)\n\tif err != nil {\n\t\treturn Result{\n\t\t\tOut:      \"Error: could not write to file\",\n\t\t\tExitCode: ExitCodeInternalError,\n\t\t}\n\t}\n\n\tvar (\n\t\toutput   strings.Builder\n\t\texitCode int\n\t)\n\n\t// replacer for commands\n\trepl := strings.NewReplacer(\n\t\t\"<file>\", f.Name(),\n\t\t// <name>: file name without extension and without path\n\t\t\"<name>\", filepath.Base(strings.TrimSuffix(f.Name(), filepath.Ext(f.Name()))),\n\t\t\"<path>\", filepath.Dir(f.Name()),\n\t)\n\n\t// For accuracy of program execution speed, we can't put anything after\n\t// recording the start time or before recording the end time.\n\tstart := time.Now()\n\n\tfor _, c := range language.Commands {\n\t\tvar command []string\n\t\t// replace <file>, <name> and <path> in commands\n\t\tfor _, v := range c {\n\t\t\tcommand = append(command, repl.Replace(v))\n\t\t}\n\t\t// execute and write output\n\t\tcmd := exec.Command(command[0], command[1:]...)\n\t\tout, err := cmd.Output()\n\t\tif err != nil {\n\t\t\toutput.Write([]byte(err.Error()))\n\t\t} else {\n\t\t\toutput.Write(out)\n\t\t}\n\n\t\t// update status code\n\t\tif err != nil {\n\t\t\tif cmd.ProcessState != nil {\n\t\t\t\texitCode = cmd.ProcessState.ExitCode()\n\t\t\t} else {\n\t\t\t\texitCode = 1 // non-zero\n\t\t\t}\n\t\t}\n\t}\n\n\tend := time.Now()\n\n\treturn Result{\n\t\tOut:           output.String(),\n\t\tExitCode:      exitCode,\n\t\tExecutionTime: end.Sub(start),\n\t}\n}\n"
  },
  {
    "path": "internal/code/code_test.go",
    "content": "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\ttt := []struct {\n\t\tmarkdown string\n\t\texpected []code.Block\n\t}{\n\t\t// We can't put backticks ```\n\t\t// in multi-line strings, ~~~ instead\n\t\t{\n\t\t\tmarkdown: `\n~~~ruby\nputs \"Hello, world!\"\n~~~\n`,\n\t\t\texpected: []code.Block{\n\t\t\t\t{\n\t\t\t\t\tCode:     `puts \"Hello, world!\"`,\n\t\t\t\t\tLanguage: \"ruby\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmarkdown: `\n~~~go\nfmt.Println(\"Hello, world!\")\n~~~\n`,\n\t\t\texpected: []code.Block{\n\t\t\t\t{\n\t\t\t\t\tCode:     `fmt.Println(\"Hello, world!\")`,\n\t\t\t\t\tLanguage: \"go\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmarkdown: `\n~~~python\nprint(\"Hello, world!\")\n~~~`,\n\t\t\texpected: []code.Block{\n\t\t\t\t{\n\t\t\t\t\tCode:     `print(\"Hello, world!\")`,\n\t\t\t\t\tLanguage: \"python\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmarkdown: `\n# Welcome to Slides\n\nA terminal based presentation tool\n\n~~~go\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n  fmt.Println(\"Written in Go!\")\n}\n~~~\n`,\n\t\t\texpected: []code.Block{\n\t\t\t\t{\n\t\t\t\t\tCode: `package main\n\nimport \"fmt\"\n\nfunc main() {\n  fmt.Println(\"Written in Go!\")\n}`,\n\t\t\t\t\tLanguage: \"go\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmarkdown: `\n# Slide 1\nJust a regular slide, no code block\n`,\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tmarkdown: ``,\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tmarkdown: `\n~~~ruby\nputs \"Hello, world!\"\n~~~\n\n~~~go\nfmt.Println(\"Hello, world!\")\n~~~\n`,\n\t\t\texpected: []code.Block{\n\t\t\t\t{\n\t\t\t\t\tCode:     `puts \"Hello, world!\"`,\n\t\t\t\t\tLanguage: \"ruby\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCode:     `fmt.Println(\"Hello, world!\")`,\n\t\t\t\t\tLanguage: \"go\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range tt {\n\t\tblocks, _ := code.Parse(tc.markdown)\n\t\tif len(blocks) != len(tc.expected) {\n\t\t\tt.Errorf(\"parse fail: incorrect size of blocks\")\n\t\t}\n\t\tfor i, block := range blocks {\n\t\t\texpected := tc.expected[i]\n\t\t\tif block.Code != expected.Code {\n\t\t\t\tt.Log(block.Code)\n\t\t\t\tt.Log(expected.Code)\n\t\t\t\tt.Fatal(\"parse failed: incorrect code\")\n\t\t\t}\n\t\t\tif block.Language != expected.Language {\n\t\t\t\tt.Fatalf(\"incorrect language, got %s, want %s\", block.Language, expected.Language)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/code/comments.go",
    "content": "package code\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n)\n\nconst comment = \"///\"\n\nvar commentRegexp = regexp.MustCompile(\"(?m)[\\r\\n]+^\" + comment + \".*$\")\n\n// HideComments removes all comments from the given content.\nfunc HideComments(content string) string {\n\treturn commentRegexp.ReplaceAllString(content, \"\")\n}\n\n// RemoveComments strips all the comments from the given content.\n// This is useful for when we want to actually use the content of the comments.\nfunc RemoveComments(content string) string {\n\treturn strings.ReplaceAll(content, comment, \"\")\n}\n"
  },
  {
    "path": "internal/code/comments_test.go",
    "content": "package code\n\nimport \"testing\"\n\nfunc TestHidesComments(t *testing.T) {\n\tcontent := `\n///package main\n///\n///import \"fmt\"\n///\n///func main() {\n  fmt.Println(\"Hello, world!\")\n///}`\n\n\texpected := `\n  fmt.Println(\"Hello, world!\")`\n\n\tif HideComments(content) != expected {\n\t\tt.Errorf(\"Expected %s, got %s\", expected, HideComments(content))\n\t}\n}\n\nfunc TestNoComments(t *testing.T) {\n\tcontent := `\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n  fmt.Println(\"Hello, world!\")\n}`\n\texpected := content\n\n\tif HideComments(content) != expected {\n\t\tt.Errorf(\"Expected %s, got %s\", expected, HideComments(content))\n\t}\n\tif RemoveComments(content) != expected {\n\t\tt.Errorf(\"Expected %s, got %s\", expected, HideComments(content))\n\t}\n}\n\nfunc TestRemoveComments(t *testing.T) {\n\tcontent := `\n///package main\n///\n///import \"fmt\"\n///\n///func main() {\n  fmt.Println(\"Hello, world!\")\n///}`\n\n\texpected := `\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n  fmt.Println(\"Hello, world!\")\n}`\n\n\tif RemoveComments(content) != expected {\n\t\tt.Errorf(\"Expected %s, got %s\", expected, RemoveComments(content))\n\t}\n}\n"
  },
  {
    "path": "internal/code/execute_test.go",
    "content": "package code_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/maaslalani/slides/internal/code\"\n)\n\nfunc TestExecute(t *testing.T) {\n\ttt := []struct {\n\t\tblock    code.Block\n\t\texpected code.Result\n\t}{\n\t\t{\n\t\t\tblock: code.Block{\n\t\t\t\tCode: `\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n  fmt.Print(\"Hello, go!\")\n}\n        `,\n\t\t\t\tLanguage: \"go\",\n\t\t\t},\n\t\t\texpected: code.Result{\n\t\t\t\tOut:      \"Hello, go!\",\n\t\t\t\tExitCode: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tblock: code.Block{\n\t\t\t\tCode:     `echo \"Hello, bash!\"`,\n\t\t\t\tLanguage: \"bash\",\n\t\t\t},\n\t\t\texpected: code.Result{\n\t\t\t\tOut:      \"Hello, bash!\\n\",\n\t\t\t\tExitCode: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tblock: code.Block{\n\t\t\t\tCode:     `Invalid Code`,\n\t\t\t\tLanguage: \"bash\",\n\t\t\t},\n\t\t\texpected: code.Result{\n\t\t\t\tOut:      \"exit status 127\",\n\t\t\t\tExitCode: 127,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tblock: code.Block{\n\t\t\t\tCode:     `Invalid Code`,\n\t\t\t\tLanguage: \"invalid\",\n\t\t\t},\n\t\t\texpected: code.Result{\n\t\t\t\tOut:      \"Error: unsupported language\",\n\t\t\t\tExitCode: code.ExitCodeInternalError,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range tt {\n\t\tr := code.Execute(tc.block)\n\t\tif r.Out != tc.expected.Out {\n\t\t\tt.Fatalf(\"invalid output for lang %s, got %s, want %s | %+v\",\n\t\t\t\ttc.block.Language, r.Out, tc.expected.Out, r)\n\t\t}\n\n\t\tif r.ExitCode != tc.expected.ExitCode {\n\t\t\tt.Fatalf(\"unexpected exit code, got %d, want %d\", r.ExitCode, tc.expected.ExitCode)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/code/languages.go",
    "content": "package code\n\n// cmds: Multiple commands; placeholders can be used\n// Placeholders <file>, <name> and <path> can be used.\ntype cmds [][]string\n\n// Language represents a programming language with it Extension and Commands to\n// execute its programs.\ntype Language struct {\n\t// Extension represents the file extension used by this language.\n\tExtension string\n\t// Commands  [][]string // placeholders: <name> file name (without\n\t// extension), <file> file name, <path> path without file name\n\tCommands cmds\n}\n\n// Supported Languages\nconst (\n\tBash       = \"bash\"\n\tZsh        = \"zsh\"\n\tFish       = \"fish\"\n\tElixir     = \"elixir\"\n\tGo         = \"go\"\n\tJavascript = \"javascript\"\n\tLua        = \"lua\"\n\tOCaml      = \"ocaml\"\n\tPerl       = \"perl\"\n\tPython     = \"python\"\n\tRuby       = \"ruby\"\n\tRust       = \"rust\"\n\tJava       = \"java\"\n\tJulia      = \"julia\"\n\tCpp        = \"cpp\"\n\tSwift      = \"swift\"\n\tDart       = \"dart\"\n\tV          = \"v\"\n\tScala      = \"scala\"\n\tHaskell    = \"haskell\"\n)\n\n// Languages is a map of supported languages with their extensions and commands\n// to run to execute the program.\nvar Languages = map[string]Language{\n\tBash: {\n\t\tExtension: \"sh\",\n\t\tCommands:  cmds{{\"bash\", \"<file>\"}},\n\t},\n\tZsh: {\n\t\tExtension: \"zsh\",\n\t\tCommands:  cmds{{\"zsh\", \"<file>\"}},\n\t},\n\tFish: {\n\t\tExtension: \"fish\",\n\t\tCommands:  cmds{{\"fish\", \"<file>\"}},\n\t},\n\tElixir: {\n\t\tExtension: \"exs\",\n\t\tCommands:  cmds{{\"elixir\", \"<file>\"}},\n\t},\n\tGo: {\n\t\tExtension: \"go\",\n\t\tCommands:  cmds{{\"go\", \"run\", \"<file>\"}},\n\t},\n\tJavascript: {\n\t\tExtension: \"js\",\n\t\tCommands:  cmds{{\"node\", \"<file>\"}},\n\t},\n\tLua: {\n\t\tExtension: \"lua\",\n\t\tCommands:  cmds{{\"lua\", \"<file>\"}},\n\t},\n\tRuby: {\n\t\tExtension: \"rb\",\n\t\tCommands:  cmds{{\"ruby\", \"<file>\"}},\n\t},\n\tOCaml: {\n\t\tExtension: \"ml\",\n\t\tCommands:  cmds{{\"ocaml\", \"<file>\"}},\n\t},\n\tPython: {\n\t\tExtension: \"py\",\n\t\tCommands:  cmds{{\"python\", \"<file>\"}},\n\t},\n\tPerl: {\n\t\tExtension: \"pl\",\n\t\tCommands:  cmds{{\"perl\", \"<file>\"}},\n\t},\n\tRust: {\n\t\tExtension: \"rs\",\n\t\tCommands: cmds{\n\t\t\t// compile code\n\t\t\t{\"rustc\", \"<file>\", \"-o\", \"<path>/<name>.run\"},\n\t\t\t// run compiled file\n\t\t\t{\"<path>/<name>.run\"},\n\t\t},\n\t},\n\tJava: {\n\t\tExtension: \"java\",\n\t\tCommands:  cmds{{\"java\", \"<file>\"}},\n\t},\n\tJulia: {\n\t\tExtension: \"jl\",\n\t\tCommands:  cmds{{\"julia\", \"<file>\"}},\n\t},\n\tCpp: {\n\t\tExtension: \"cpp\",\n\t\tCommands: cmds{\n\t\t\t{\"g++\", \"-std=c++20\", \"-o\", \"<path>/<name>.run\", \"<file>\"},\n\t\t\t{\"<path>/<name>.run\"},\n\t\t},\n\t},\n\tSwift: {\n\t\tExtension: \"swift\",\n\t\tCommands:  cmds{{\"swift\", \"<file>\"}},\n\t},\n\tDart: {\n\t\tExtension: \"dart\",\n\t\tCommands:  cmds{{\"dart\", \"<file>\"}},\n\t},\n\tV: {\n\t\tExtension: \"v\",\n\t\tCommands:  cmds{{\"v\", \"run\", \"<file>\"}},\n\t},\n\tScala: {\n\t\tExtension: \"sc\",\n\t\tCommands: cmds{{\"scala-cli\", \"run\", \"<file>\"}},\n\t},\n\tHaskell: {\n\t\tExtension: \"hs\",\n\t\tCommands: cmds{{\"runghc\", \"<file>\"}},\n\t},\n}\n"
  },
  {
    "path": "internal/file/file.go",
    "content": "// 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// Exists is a helper to verify\n// that the provided filepath exists\n// on the system\nfunc Exists(filepath string) bool {\n\tinfo, err := os.Stat(filepath)\n\tif os.IsNotExist(err) {\n\t\treturn false\n\t}\n\treturn !info.IsDir()\n}\n\n// IsExecutable returns whether a file has execution permissions\nfunc IsExecutable(s fs.FileInfo) bool {\n\treturn s.Mode().Perm()&0111 == 0111\n}\n"
  },
  {
    "path": "internal/file/file_test.go",
    "content": "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/stretchr/testify/assert\"\n)\n\nfunc TestExists(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfilepath string\n\t\twant     bool\n\t}{\n\t\t{name: \"Find file exists\", filepath: \"file.go\", want: true},\n\t\t{name: \"Return false for missing file\", filepath: \"afilethatdoesntexist.go\", want: false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tisExist := file.Exists(tt.filepath)\n\t\t\tif isExist {\n\t\t\t\tassert.FileExists(t, tt.filepath)\n\t\t\t}\n\t\t\tassert.Equal(t, tt.want, isExist)\n\t\t})\n\t}\n}\n\nfunc TestIsExecutable(t *testing.T) {\n\ttests := []struct {\n\t\tperm     fs.FileMode\n\t\texpected bool\n\t}{\n\t\t{0101, false},\n\t\t{0111, true},\n\t\t{0644, false},\n\t\t{0666, false},\n\t\t{0777, true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(fmt.Sprint(tc.perm), func(t *testing.T) {\n\t\t\ttmp, err := os.CreateTemp(os.TempDir(), \"slides-*\")\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"failed to create temp file\")\n\t\t\t}\n\t\t\tdefer os.Remove(tmp.Name())\n\n\t\t\terr = tmp.Chmod(tc.perm)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\ts, err := tmp.Stat()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"failed to stat file\")\n\t\t\t}\n\n\t\t\twant := tc.expected\n\t\t\tgot := file.IsExecutable(s)\n\t\t\tif tc.expected != got {\n\t\t\t\tt.Log(want)\n\t\t\t\tt.Log(got)\n\t\t\t\tt.Fatalf(\"IsExecutable returned an incorrect result, want: %t, got %t\", want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/meta/meta.go",
    "content": "// Package meta implements markdown frontmatter parsing for simple\n// slides configuration\npackage meta\n\nimport (\n\t\"os\"\n\t\"os/user\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gopkg.in/yaml.v2\"\n)\n\n// Temporary structure to differentiate values not present in the YAML header\n// from values set to empty strings in the YAML header. We replace values not\n// set by defaults values when parsing a header.\ntype parsedMeta struct {\n\tTheme  *string `yaml:\"theme\"`\n\tAuthor *string `yaml:\"author\"`\n\tDate   *string `yaml:\"date\"`\n\tPaging *string `yaml:\"paging\"`\n}\n\n// Meta contains all of the data to be parsed\n// out of a markdown file's header section\ntype Meta struct {\n\tTheme  string\n\tAuthor string\n\tDate   string\n\tPaging string\n}\n\n// New creates a new instance of the\n// slideshow meta header object\nfunc New() *Meta {\n\treturn &Meta{}\n}\n\n// Parse parses metadata from a slideshows header slide\n// including theme information\n//\n// If no front matter is provided, it will fallback to the default theme and\n// return false to acknowledge that there is no front matter in this slide\nfunc (m *Meta) Parse(header string) (*Meta, bool) {\n\tfallback := &Meta{\n\t\tTheme:  defaultTheme(),\n\t\tAuthor: defaultAuthor(),\n\t\tDate:   defaultDate(),\n\t\tPaging: defaultPaging(),\n\t}\n\n\tvar tmp parsedMeta\n\terr := yaml.Unmarshal([]byte(header), &tmp)\n\tif err != nil {\n\t\treturn fallback, false\n\t}\n\n\tif tmp.Theme != nil {\n\t\tm.Theme = *tmp.Theme\n\t} else {\n\t\tm.Theme = fallback.Theme\n\t}\n\n\tif tmp.Author != nil {\n\t\tm.Author = *tmp.Author\n\t} else {\n\t\tm.Author = fallback.Author\n\t}\n\n\tif tmp.Date != nil {\n\t\tparsedDate := parseDate(*tmp.Date)\n\t\tif parsedDate == *tmp.Date {\n\t\t\tm.Date = *tmp.Date\n\t\t} else {\n\t\t\tm.Date = time.Now().Format(parsedDate)\n\t\t}\n\t} else {\n\t\tm.Date = fallback.Date\n\t}\n\n\tif tmp.Paging != nil {\n\t\tm.Paging = *tmp.Paging\n\t} else {\n\t\tm.Paging = fallback.Paging\n\t}\n\n\treturn m, true\n}\n\nfunc defaultTheme() string {\n\ttheme := os.Getenv(\"GLAMOUR_STYLE\")\n\tif theme == \"\" {\n\t\treturn \"default\"\n\t}\n\treturn theme\n}\n\nfunc defaultAuthor() string {\n\tuser, err := user.Current()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn user.Name\n}\n\nfunc defaultDate() string {\n\treturn time.Now().Format(parseDate(\"YYYY-MM-DD\"))\n}\n\nfunc defaultPaging() string {\n\treturn \"Slide %d / %d\"\n}\n\nfunc parseDate(value string) string {\n\tpairs := [][]string{\n\t\t{\"YYYY\", \"2006\"},\n\t\t{\"YY\", \"06\"},\n\t\t{\"MMMM\", \"January\"},\n\t\t{\"MMM\", \"Jan\"},\n\t\t{\"MM\", \"01\"},\n\t\t{\"mm\", \"1\"},\n\t\t{\"DD\", \"02\"},\n\t\t{\"dd\", \"2\"},\n\t}\n\n\tfor _, p := range pairs {\n\t\tvalue = strings.ReplaceAll(value, p[0], p[1])\n\t}\n\treturn value\n}\n"
  },
  {
    "path": "internal/meta/meta_test.go",
    "content": "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.com/stretchr/testify/assert\"\n)\n\nfunc TestMeta_ParseHeader(t *testing.T) {\n\tuser, _ := user.Current()\n\tdate := time.Now().Format(\"2006-01-02\")\n\n\ttests := []struct {\n\t\tname      string\n\t\tslideshow string\n\t\twant      *meta.Meta\n\t}{\n\t\t{\n\t\t\tname:      \"Parse theme from header\",\n\t\t\tslideshow: fmt.Sprintf(\"---\\ntheme: %q\\n\", \"dark\"),\n\t\t\twant: &meta.Meta{\n\t\t\t\tTheme:  \"dark\",\n\t\t\t\tAuthor: user.Name,\n\t\t\t\tDate:   date,\n\t\t\t\tPaging: \"Slide %d / %d\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Fallback to default if no theme provided\",\n\t\t\tslideshow: \"\\n# Header Slide\\n > Subtitle\\n\",\n\t\t\twant: &meta.Meta{\n\t\t\t\tTheme:  \"default\",\n\t\t\t\tAuthor: user.Name,\n\t\t\t\tDate:   date,\n\t\t\t\tPaging: \"Slide %d / %d\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Parse author from header\",\n\t\t\tslideshow: fmt.Sprintf(\"---\\nauthor: %q\\n\", \"gopher\"),\n\t\t\twant: &meta.Meta{\n\t\t\t\tTheme:  \"default\",\n\t\t\t\tAuthor: \"gopher\",\n\t\t\t\tDate:   date,\n\t\t\t\tPaging: \"Slide %d / %d\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Fallback to default if no author provided\",\n\t\t\tslideshow: \"\\n# Header Slide\\n > Subtitle\\n\",\n\t\t\twant: &meta.Meta{\n\t\t\t\tTheme:  \"default\",\n\t\t\t\tAuthor: user.Name,\n\t\t\t\tDate:   date,\n\t\t\t\tPaging: \"Slide %d / %d\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Parse static date from header\",\n\t\t\tslideshow: fmt.Sprintf(\"---\\ndate: %q\\n\", \"31/01/1970\"),\n\t\t\twant: &meta.Meta{\n\t\t\t\tTheme:  \"default\",\n\t\t\t\tAuthor: user.Name,\n\t\t\t\tDate:   \"31/01/1970\",\n\t\t\t\tPaging: \"Slide %d / %d\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Parse go-styled date from header\",\n\t\t\tslideshow: fmt.Sprintf(\"---\\ndate: %q\\n\", \"MMM dd, YYYY\"),\n\t\t\twant: &meta.Meta{\n\t\t\t\tTheme:  \"default\",\n\t\t\t\tAuthor: user.Name,\n\t\t\t\tDate:   time.Now().Format(\"Jan 2, 2006\"),\n\t\t\t\tPaging: \"Slide %d / %d\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Parse YYYY-MM-DD date from header\",\n\t\t\tslideshow: fmt.Sprintf(\"---\\ndate: %q\\n\", \"YYYY-MM-DD\"),\n\t\t\twant: &meta.Meta{\n\t\t\t\tTheme:  \"default\",\n\t\t\t\tAuthor: user.Name,\n\t\t\t\tDate:   time.Now().Format(\"2006-01-02\"),\n\t\t\t\tPaging: \"Slide %d / %d\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Parse dd/mm/YY date from header\",\n\t\t\tslideshow: fmt.Sprintf(\"---\\ndate: %q\\n\", \"dd/mm/YY\"),\n\t\t\twant: &meta.Meta{\n\t\t\t\tTheme:  \"default\",\n\t\t\t\tAuthor: user.Name,\n\t\t\t\tDate:   time.Now().Format(\"2/1/06\"),\n\t\t\t\tPaging: \"Slide %d / %d\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Parse MMM dd, YYYY date from header\",\n\t\t\tslideshow: fmt.Sprintf(\"---\\ndate: %q\\n\", \"MMM dd, YYYY\"),\n\t\t\twant: &meta.Meta{\n\t\t\t\tTheme:  \"default\",\n\t\t\t\tAuthor: user.Name,\n\t\t\t\tDate:   time.Now().Format(\"Jan 2, 2006\"),\n\t\t\t\tPaging: \"Slide %d / %d\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Parse MMMM DD, YYYY date from header\",\n\t\t\tslideshow: fmt.Sprintf(\"---\\ndate: %q\\n\", \"MMMM DD, YYYY\"),\n\t\t\twant: &meta.Meta{\n\t\t\t\tTheme:  \"default\",\n\t\t\t\tAuthor: user.Name,\n\t\t\t\tDate:   time.Now().Format(\"January 02, 2006\"),\n\t\t\t\tPaging: \"Slide %d / %d\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Fallback to default if no date provided\",\n\t\t\tslideshow: \"\\n# Header Slide\\n > Subtitle\\n\",\n\t\t\twant: &meta.Meta{\n\t\t\t\tTheme:  \"default\",\n\t\t\t\tAuthor: user.Name,\n\t\t\t\tDate:   date,\n\t\t\t\tPaging: \"Slide %d / %d\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Parse paging from header\",\n\t\t\tslideshow: fmt.Sprintf(\"---\\npaging: %q\\n\", \"%d of %d\"),\n\t\t\twant: &meta.Meta{\n\t\t\t\tTheme:  \"default\",\n\t\t\t\tAuthor: user.Name,\n\t\t\t\tDate:   date,\n\t\t\t\tPaging: \"%d of %d\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Fallback to default if no numebring provided\",\n\t\t\tslideshow: \"\\n# Header Slide\\n > Subtitle\\n\",\n\t\t\twant: &meta.Meta{\n\t\t\t\tTheme:  \"default\",\n\t\t\t\tAuthor: user.Name,\n\t\t\t\tDate:   date,\n\t\t\t\tPaging: \"Slide %d / %d\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Fallback if first slide is valid yaml\",\n\t\t\tslideshow: \"---\\n# Header Slide---\\nContent\\n\",\n\t\t\twant: &meta.Meta{\n\t\t\t\tTheme:  \"default\",\n\t\t\t\tAuthor: user.Name,\n\t\t\t\tDate:   date,\n\t\t\t\tPaging: \"Slide %d / %d\",\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := &meta.Meta{}\n\t\t\tgot, hasMeta := m.Parse(tt.slideshow)\n\t\t\tif !hasMeta {\n\t\t\t\tassert.NotNil(t, got)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestNew(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\twant *meta.Meta\n\t}{\n\t\t{name: \"Create meta struct\", want: &meta.Meta{}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, meta.New(), tt.want)\n\t\t})\n\t}\n}\n\nfunc ExampleMeta_Parse() {\n\theader := `\n---\ntheme: \"dark\"\nauthor: \"Gopher\"\ndate: \"Apr. 4, 2021\"\npaging: \"%d\"\n---\n`\n\t// Parse the header from the markdown\n\t// file\n\tm, _ := meta.New().Parse(header)\n\n\t// Print the return theme\n\t// meta\n\tfmt.Println(m.Theme)\n\tfmt.Println(m.Author)\n\tfmt.Println(m.Date)\n\tfmt.Println(m.Paging)\n}\n"
  },
  {
    "path": "internal/model/model.go",
    "content": "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/clipboard\"\n\t\"github.com/maaslalani/slides/internal/file\"\n\t\"github.com/maaslalani/slides/internal/navigation\"\n\t\"github.com/maaslalani/slides/internal/process\"\n\n\t\"github.com/charmbracelet/bubbles/viewport\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/glamour\"\n\t\"github.com/maaslalani/slides/internal/code\"\n\t\"github.com/maaslalani/slides/internal/meta\"\n\t\"github.com/maaslalani/slides/styles\"\n)\n\nvar (\n\t//go:embed tutorial.md\n\tslidesTutorial []byte\n\ttabSpaces      = strings.Repeat(\" \", 4)\n)\n\nconst (\n\tdelimiter = \"\\n---\\n\"\n)\n\n// Model represents the model of this presentation, which contains all the\n// state related to the current slides.\ntype Model struct {\n\tSlides   []string\n\tPage     int\n\tAuthor   string\n\tDate     string\n\tTheme    glamour.TermRendererOption\n\tPaging   string\n\tFileName string\n\tviewport viewport.Model\n\tbuffer   string\n\t// VirtualText is used for additional information that is not part of the\n\t// original slides, it will be displayed on a slide and reset on page change\n\tVirtualText string\n\tSearch      navigation.Search\n}\n\ntype fileWatchMsg struct{}\n\nvar fileInfo os.FileInfo\n\n// Init initializes the model and begins watching the slides file for changes\n// if it exists.\nfunc (m Model) Init() tea.Cmd {\n\tif m.FileName == \"\" {\n\t\treturn nil\n\t}\n\tfileInfo, _ = os.Stat(m.FileName)\n\treturn fileWatchCmd()\n}\n\nfunc fileWatchCmd() tea.Cmd {\n\treturn tea.Every(time.Second, func(t time.Time) tea.Msg {\n\t\treturn fileWatchMsg{}\n\t})\n}\n\n// Load loads all of the content and metadata for the presentation.\nfunc (m *Model) Load() error {\n\tvar content string\n\tvar err error\n\n\tif m.FileName != \"\" {\n\t\tcontent, err = readFile(m.FileName)\n\t} else {\n\t\tcontent, err = readStdin()\n\t}\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcontent = strings.ReplaceAll(content, \"\\r\", \"\")\n\n\tcontent = strings.TrimPrefix(content, strings.TrimPrefix(delimiter, \"\\n\"))\n\tslides := strings.Split(content, delimiter)\n\n\tmetaData, exists := meta.New().Parse(slides[0])\n\t// If the user specifies a custom configuration options\n\t// skip the first \"slide\" since this is all configuration\n\tif exists && len(slides) > 1 {\n\t\tslides = slides[1:]\n\t}\n\n\tm.Slides = slides\n\tm.Author = metaData.Author\n\tm.Date = metaData.Date\n\tm.Paging = metaData.Paging\n\tif m.Theme == nil {\n\t\tm.Theme = styles.SelectTheme(metaData.Theme)\n\t}\n\n\treturn nil\n}\n\n// Update updates the presentation model.\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase tea.WindowSizeMsg:\n\t\tm.viewport.Width = msg.Width\n\t\tm.viewport.Height = msg.Height\n\t\treturn m, nil\n\n\tcase tea.KeyMsg:\n\t\tkeyPress := msg.String()\n\n\t\tif m.Search.Active {\n\t\t\tswitch msg.Type {\n\t\t\tcase tea.KeyEnter:\n\t\t\t\t// execute current buffer\n\t\t\t\tif m.Search.Query() != \"\" {\n\t\t\t\t\tm.Search.Execute(&m)\n\t\t\t\t} else {\n\t\t\t\t\tm.Search.Done()\n\t\t\t\t}\n\t\t\t\t// cancel search\n\t\t\t\treturn m, nil\n\t\t\tcase tea.KeyCtrlC, tea.KeyEscape:\n\t\t\t\t// quit command mode\n\t\t\t\tm.Search.SetQuery(\"\")\n\t\t\t\tm.Search.Done()\n\t\t\t\treturn m, nil\n\t\t\t}\n\n\t\t\tvar cmd tea.Cmd\n\t\t\tm.Search.SearchTextInput, cmd = m.Search.SearchTextInput.Update(msg)\n\t\t\treturn m, cmd\n\t\t}\n\n\t\tswitch keyPress {\n\t\tcase \"/\":\n\t\t\t// Begin search\n\t\t\tm.Search.Begin()\n\t\t\tm.Search.SearchTextInput.Focus()\n\t\t\treturn m, nil\n\t\tcase \"ctrl+n\":\n\t\t\t// Go to next occurrence\n\t\t\tm.Search.Execute(&m)\n\t\tcase \"ctrl+e\":\n\t\t\t// Run code blocks\n\t\t\tblocks, err := code.Parse(m.Slides[m.Page])\n\t\t\tif err != nil {\n\t\t\t\t// We couldn't parse the code block on the screen\n\t\t\t\tm.VirtualText = \"\\n\" + err.Error()\n\t\t\t\treturn m, nil\n\t\t\t}\n\t\t\tvar outs []string\n\t\t\tfor _, block := range blocks {\n\t\t\t\tres := code.Execute(block)\n\t\t\t\touts = append(outs, res.Out)\n\t\t\t}\n\t\t\tm.VirtualText = strings.Join(outs, \"\\n\")\n\t\tcase \"y\":\n\t\t\tblocks, err := code.Parse(m.Slides[m.Page])\n\t\t\tif err != nil {\n\t\t\t\treturn m, nil\n\t\t\t}\n\t\t\tfor _, b := range blocks {\n\t\t\t\t_ = clipboard.WriteAll(b.Code)\n\t\t\t}\n\t\t\treturn m, nil\n\t\tcase \"ctrl+c\", \"q\":\n\t\t\treturn m, tea.Quit\n\t\tdefault:\n\t\t\tnewState := navigation.Navigate(navigation.State{\n\t\t\t\tBuffer:      m.buffer,\n\t\t\t\tPage:        m.Page,\n\t\t\t\tTotalSlides: len(m.Slides),\n\t\t\t}, keyPress)\n\t\t\tm.buffer = newState.Buffer\n\t\t\tm.SetPage(newState.Page)\n\t\t}\n\n\tcase fileWatchMsg:\n\t\tnewFileInfo, err := os.Stat(m.FileName)\n\t\tif err == nil && newFileInfo.ModTime() != fileInfo.ModTime() {\n\t\t\tfileInfo = newFileInfo\n\t\t\t_ = m.Load()\n\t\t\tif m.Page >= len(m.Slides) {\n\t\t\t\tm.Page = len(m.Slides) - 1\n\t\t\t}\n\t\t}\n\t\treturn m, fileWatchCmd()\n\t}\n\treturn m, nil\n}\n\n// View renders the current slide in the presentation and the status bar which\n// contains the author, date, and pagination information.\nfunc (m Model) View() string {\n\tr, _ := glamour.NewTermRenderer(m.Theme, glamour.WithWordWrap(m.viewport.Width))\n\tslide := m.Slides[m.Page]\n\tslide = code.HideComments(slide)\n\tslide, err := r.Render(slide)\n\tslide = strings.ReplaceAll(slide, \"\\t\", tabSpaces)\n\tslide += m.VirtualText\n\tif err != nil {\n\t\tslide = fmt.Sprintf(\"Error: Could not render markdown! (%v)\", err)\n\t}\n\tslide = styles.Slide.Render(slide)\n\n\tvar left string\n\tif m.Search.Active {\n\t\t// render search bar\n\t\tleft = m.Search.SearchTextInput.View()\n\t} else {\n\t\t// render author and date\n\t\tleft = styles.Author.Render(m.Author) + styles.Date.Render(m.Date)\n\t}\n\n\tright := styles.Page.Render(m.paging())\n\tstatus := styles.Status.Render(styles.JoinHorizontal(left, right, m.viewport.Width))\n\treturn styles.JoinVertical(slide, status, m.viewport.Height)\n}\n\nfunc (m *Model) paging() string {\n\tswitch strings.Count(m.Paging, \"%d\") {\n\tcase 2:\n\t\treturn fmt.Sprintf(m.Paging, m.Page+1, len(m.Slides))\n\tcase 1:\n\t\treturn fmt.Sprintf(m.Paging, m.Page+1)\n\tdefault:\n\t\treturn m.Paging\n\t}\n}\n\nfunc readFile(path string) (string, error) {\n\ts, err := os.Stat(path)\n\tif err != nil {\n\t\treturn \"\", errors.New(\"could not read file\")\n\t}\n\tif s.IsDir() {\n\t\treturn \"\", errors.New(\"can not read directory\")\n\t}\n\tb, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcontent := string(b)\n\n\t// Pre-process slides if the file is executable to avoid\n\t// unintentional code execution when presenting slides\n\tif file.IsExecutable(s) {\n\t\t// Remove shebang if file has one\n\t\tif strings.HasPrefix(content, \"#!\") {\n\t\t\tcontent = strings.Join(strings.SplitN(content, \"\\n\", 2)[1:], \"\\n\")\n\t\t}\n\n\t\tcontent = process.Pre(content)\n\t}\n\n\treturn content, err\n}\n\nfunc readStdin() (string, error) {\n\tstat, err := os.Stdin.Stat()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif stat.Mode()&os.ModeNamedPipe == 0 && stat.Size() == 0 {\n\t\treturn string(slidesTutorial), nil\n\t}\n\n\treader := bufio.NewReader(os.Stdin)\n\tvar b strings.Builder\n\n\tfor {\n\t\tr, _, err := reader.ReadRune()\n\t\tif err != nil && err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\t_, err = b.WriteRune(r)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\treturn b.String(), nil\n}\n\n// CurrentPage returns the current page the presentation is on.\nfunc (m *Model) CurrentPage() int {\n\treturn m.Page\n}\n\n// SetPage sets which page the presentation should render.\nfunc (m *Model) SetPage(page int) {\n\tif m.Page == page {\n\t\treturn\n\t}\n\n\tm.VirtualText = \"\"\n\tm.Page = page\n}\n\n// Pages returns all the slides in the presentation.\nfunc (m *Model) Pages() []string {\n\treturn m.Slides\n}\n"
  },
  {
    "path": "internal/model/tutorial.md",
    "content": "# Welcome to Slides\nA terminal based presentation tool\n\n## Everything is markdown\nIn fact this entire presentation is a markdown file.\n\nPress `n` to go to the next slide.\n\n---\n\n# Display Code\n\n```go\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n  // You can show code in slides\n  // Press ctrl+e to execute this code directly in slides\n  fmt.Println(\"Tada!\")\n}\n```\n\n---\n\n# h1\n\nYou can use everything in markdown!\n* Like bulleted list\n* You know the deal\n\n1. Numbered lists too\n\n## h2\n\n| Tables | Too    |\n| ------ | ------ |\n| Even   | Tables |\n\n\n### h3\n\n#### h4\n##### h5\n###### h6\n\n---\n\n# Graphs\n\n```\ndigraph {\n    rankdir = LR;\n    a -> b;\n    b -> c;\n}\n```\n```\n┌───┐     ┌───┐     ┌───┐\n│ a │ ──▶ │ b │ ──▶ │ c │\n└───┘     └───┘     └───┘\n```\n---\n\nAll you need to do is separate slides with triple dashes\n`---` on a separate line, like so:\n\n```markdown\n# Slide 1\nSome stuff\n\n--- \n\n# Slide 2\nSome other stuff\n```\n"
  },
  {
    "path": "internal/navigation/navigation.go",
    "content": "package navigation\n\nimport (\n\t\"strconv\"\n)\n\ntype repeatableFunc func(slide, totalSlides int) int\n\n// State tracks the current buffer, page, and total number of slides\ntype State struct {\n\tBuffer      string\n\tPage        int\n\tTotalSlides int\n}\n\n// Navigate receives the current State and keyPress, and returns the new State.\nfunc Navigate(state State, keyPress string) State {\n\tswitch keyPress {\n\tcase \"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\":\n\t\tnewBuffer := keyPress\n\n\t\tif bufferIsNumeric(state.Buffer) {\n\t\t\tnewBuffer = state.Buffer + keyPress\n\t\t}\n\n\t\treturn State{\n\t\t\tBuffer:      newBuffer,\n\t\t\tPage:        state.Page,\n\t\t\tTotalSlides: state.TotalSlides,\n\t\t}\n\tcase \"g\":\n\t\tswitch state.Buffer {\n\t\tcase \"g\":\n\t\t\treturn State{\n\t\t\t\tPage:        0,\n\t\t\t\tTotalSlides: state.TotalSlides,\n\t\t\t}\n\t\tdefault:\n\t\t\treturn State{\n\t\t\t\tBuffer:      \"g\",\n\t\t\t\tPage:        state.Page,\n\t\t\t\tTotalSlides: state.TotalSlides,\n\t\t\t}\n\t\t}\n\tcase \"G\":\n\t\ttargetSlide := state.TotalSlides - 1\n\t\tif bufferIsNumeric(state.Buffer) {\n\t\t\ttargetSlide = navigateSlide(state.Buffer, state.TotalSlides)\n\t\t}\n\n\t\treturn State{\n\t\t\tPage:        targetSlide,\n\t\t\tTotalSlides: state.TotalSlides,\n\t\t}\n\tcase \" \", \"down\", \"j\", \"right\", \"l\", \"enter\", \"n\", \"pgdown\":\n\t\treturn State{\n\t\t\tPage:        navigateNext(state),\n\t\t\tTotalSlides: state.TotalSlides,\n\t\t}\n\tcase \"up\", \"k\", \"left\", \"h\", \"p\", \"pgup\", \"N\":\n\t\treturn State{\n\t\t\tPage:        navigatePrevious(state),\n\t\t\tTotalSlides: state.TotalSlides,\n\t\t}\n\tdefault:\n\t\treturn State{\n\t\t\tPage:        state.Page,\n\t\t\tTotalSlides: state.TotalSlides,\n\t\t}\n\t}\n}\n\nfunc bufferIsNumeric(buffer string) bool {\n\t_, err := strconv.Atoi(buffer)\n\treturn err == nil\n}\n\nfunc navigateNext(state State) int {\n\treturn repeatableAction(func(slide, totalSlides int) int {\n\t\tif slide < totalSlides-1 {\n\t\t\treturn slide + 1\n\t\t}\n\n\t\treturn totalSlides - 1\n\t}, state)\n}\n\nfunc navigateSlide(buffer string, totalSlides int) int {\n\tdestinationSlide, _ := strconv.Atoi(buffer)\n\tdestinationSlide--\n\n\tif destinationSlide > totalSlides-1 {\n\t\treturn totalSlides - 1\n\t}\n\n\tif destinationSlide < 0 {\n\t\treturn 0\n\t}\n\n\treturn destinationSlide\n}\n\nfunc navigatePrevious(state State) int {\n\treturn repeatableAction(func(slide, totalSlides int) int {\n\t\tif slide > 0 {\n\t\t\treturn slide - 1\n\t\t}\n\n\t\treturn slide\n\t}, state)\n}\n\nfunc repeatableAction(fn repeatableFunc, state State) int {\n\tif !bufferIsNumeric(state.Buffer) {\n\t\treturn fn(state.Page, state.TotalSlides)\n\t}\n\n\trepeat, _ := strconv.Atoi(state.Buffer)\n\tpage := state.Page\n\n\tif repeat == 0 {\n\t\t// This is how behaviour works in Vim, so following principle of least astonishment.\n\t\treturn fn(state.Page, state.TotalSlides)\n\t}\n\n\tfor i := 0; i < repeat; i++ {\n\t\tpage = fn(page, state.TotalSlides)\n\t}\n\n\treturn page\n}\n"
  },
  {
    "path": "internal/navigation/navigation_test.go",
    "content": "package navigation\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNavigation(t *testing.T) {\n\ttests := []struct {\n\t\tkeys   string\n\t\ttarget int\n\t}{\n\t\t{target: 0},\n\t\t{keys: \"l\", target: 1},\n\t\t{keys: \"jjjjjjjjjj\", target: 10},\n\t\t{keys: \"jjjjjjjjjjjjj\", target: 10},\n\t\t{keys: \"G\", target: 10},\n\t\t{keys: \"llgg\", target: 0},\n\t\t{keys: \"2j\", target: 2},\n\t\t{keys: \"0j\", target: 1},\n\t\t{keys: \"-11G\", target: 10},\n\t\t{keys: \"0G\", target: 0},\n\t\t{keys: \"3G\", target: 2},\n\t\t{keys: \"11G\", target: 10},\n\t\t{keys: \"101G\", target: 10},\n\t\t{keys: \"nnN\", target: 1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.keys, func(t *testing.T) {\n\t\t\tcurrentState := State{\n\t\t\t\tBuffer:      \"\",\n\t\t\t\tPage:        0,\n\t\t\t\tTotalSlides: 11,\n\t\t\t}\n\n\t\t\tfor _, key := range strings.Split(tt.keys, \"\") {\n\t\t\t\tcurrentState = Navigate(currentState, key)\n\t\t\t}\n\n\t\t\ttargetState := State{Page: tt.target, TotalSlides: 11}\n\t\t\tassert.Equal(t, targetState, currentState)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/navigation/search.go",
    "content": "package navigation\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\t\"github.com/maaslalani/slides/styles\"\n)\n\n// Model is an interface for models.model, so that cycle imports are avoided\ntype Model interface {\n\tCurrentPage() int\n\tSetPage(page int)\n\tPages() []string\n}\n\n// Search represents the current search\ntype Search struct {\n\t// Active - Show search bar instead of author and date?\n\t// Store keystrokes in Query?\n\tActive bool\n\t// Query stores the current \"search term\"\n\tSearchTextInput textinput.Model\n}\n\n// NewSearch creates and returns a new search model with the default settings.\nfunc NewSearch() Search {\n\tti := textinput.New()\n\tti.Placeholder = \"search\"\n\tti.Prompt = \"/\"\n\tti.PromptStyle = styles.Search\n\tti.TextStyle = styles.Search\n\treturn Search{SearchTextInput: ti}\n}\n\n// Query returns the text input's value.\nfunc (s *Search) Query() string {\n\treturn s.SearchTextInput.Value()\n}\n\n// SetQuery sets the text input's value\nfunc (s *Search) SetQuery(query string) {\n\ts.SearchTextInput.SetValue(query)\n}\n\n// Done marks the search as done, but does not delete the search buffer. This\n// is useful if, for example, you want to jump to the next result and you\n// therefore still need the buffer.\nfunc (s *Search) Done() {\n\ts.Active = false\n}\n\n// Begin a new search (deletes old buffer)\nfunc (s *Search) Begin() {\n\ts.Active = true\n\ts.SetQuery(\"\")\n}\n\n// Execute search\nfunc (s *Search) Execute(m Model) {\n\tdefer s.Done()\n\texpr := s.Query()\n\tif expr == \"\" {\n\t\treturn\n\t}\n\tif strings.HasSuffix(expr, \"/i\") {\n\t\texpr = \"(?i)\" + expr[:len(expr)-2]\n\t}\n\tpattern, err := regexp.Compile(expr)\n\tif err != nil {\n\t\treturn\n\t}\n\tcheck := func(i int) bool {\n\t\tcontent := m.Pages()[i]\n\t\tif len(pattern.FindAllStringSubmatch(content, 1)) != 0 {\n\t\t\tm.SetPage(i)\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\t// search from next slide to end\n\tfor i := m.CurrentPage() + 1; i < len(m.Pages()); i++ {\n\t\tif check(i) {\n\t\t\treturn\n\t\t}\n\t}\n\t// search from first slide to previous\n\tfor i := 0; i < m.CurrentPage(); i++ {\n\t\tif check(i) {\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/navigation/search_test.go",
    "content": "package navigation\n\nimport (\n\t\"testing\"\n)\n\ntype mockModel struct {\n\tslides []string\n\tpage   int\n}\n\nfunc (m *mockModel) CurrentPage() int {\n\treturn m.page\n}\n\nfunc (m *mockModel) SetPage(page int) {\n\tm.page = page\n}\n\nfunc (m *mockModel) Pages() []string {\n\treturn m.slides\n}\n\nfunc TestSearch(t *testing.T) {\n\tdata := []string{\n\t\t\"hi\",\n\t\t\"first\",\n\t\t\"second\",\n\t\t\"third\",\n\t\t\"AbCdEfG\",\n\t\t\"abcdefg\",\n\t\t\"seconds\",\n\t}\n\n\ttype query struct {\n\t\tdesc     string\n\t\tquery    string\n\t\texpected int\n\t}\n\n\t// query -> expected page\n\tqueries := []query{\n\t\t{\"basic 'first'\", \"first\", 1},\n\t\t{\"basic 'abc'\", \"abc\", 5},\n\t\t{\"basic 'abc' next occurrence\", \"abc\", 5},\n\t\t{\"'abc' ignore case\", \"abc/i\", 4},\n\t\t{\"'abc' ignore case\", \"abc/i\", 5},\n\t\t{\"'abc' ignore case\", \"abc/i\", 4},\n\t\t{\"next occurrence 1/2\", \"sec\", 6},\n\t\t{\"next occurrence 2/2\", \"sec\", 2},\n\t\t{\"regex\", \"a.c\", 5},\n\t\t{\"regex next occurrence\", \"a.c\", 5},\n\t\t{\"regex ignore case\", \"a.c/i\", 4},\n\t\t{\"regex ignore case next occurrence\", \"a.c/i\", 5},\n\t}\n\n\tm := &mockModel{\n\t\tslides: data,\n\t\tpage:   0,\n\t}\n\n\ts := &Search{}\n\tfor _, query := range queries {\n\t\ts.SetQuery(query.query)\n\t\ts.Execute(m)\n\t\tif m.CurrentPage() != query.expected {\n\t\t\tt.Errorf(\"[%s] expected page %d, got %d\", query.desc, query.expected, m.CurrentPage())\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "internal/process/execute_test.go",
    "content": "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\t{\n\t\t\tblock: Block{\n\t\t\t\tCommand: \"cat\",\n\t\t\t\tInput:   \"Hello, world!\",\n\t\t\t},\n\t\t\twant: \"Hello, world!\",\n\t\t},\n\t\t{\n\t\t\tblock: Block{\n\t\t\t\tCommand: \"sed -e s/Find/Replace/g\",\n\t\t\t\tInput:   \"Find\",\n\t\t\t},\n\t\t\twant: \"Replace\",\n\t\t},\n\t}\n\n\tfor _, tc := range tt {\n\t\tt.Run(tc.want, func(t *testing.T) {\n\t\t\tif testing.Short() {\n\t\t\t\tt.SkipNow()\n\t\t\t}\n\t\t\ttc.block.Execute()\n\t\t\tgot := tc.block.Output\n\t\t\tif tc.want != got {\n\t\t\t\tt.Fatalf(\"Invalid execution, want %s, got %s\", tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/process/process.go",
    "content": "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 which looks like the following: It\n// is delimited by ~~~ and contains a command to be run along with the input to\n// be passed, the entire block should be replaced with its command output\n//\n// ~~~sd block process\n// block\n// ~~~\ntype Block struct {\n\tCommand string\n\tInput   string\n\tOutput  string\n\tRaw     string\n}\n\n// String implements the Stringer interface.\nfunc (b Block) String() string {\n\treturn fmt.Sprintf(\"===\\n%s\\n%s\\n%s\\n===\", b.Raw, b.Command, b.Input)\n}\n\n// ?: means non-capture group\nvar reng = regexp.MustCompile(\"~~~(.+)\\n(?:.|\\n)*?\\n~~~\\\\s?\")\nvar reg = regexp.MustCompile(\"(?s)~~~(.+?)\\n(.*?)\\n~~~\\\\s?\")\n\n// Parse takes some markdown and returns blocks to be pre-processed\nfunc Parse(markdown string) []Block {\n\tvar blocks []Block\n\tmatches := reng.FindAllString(markdown, -1)\n\tfor _, match := range matches {\n\t\tm := reg.FindStringSubmatch(match)\n\t\tblocks = append(blocks, Block{\n\t\t\tCommand: m[1],\n\t\t\tInput:   m[2],\n\t\t\tRaw:     strings.TrimSuffix(m[0], \"\\n\"),\n\t\t})\n\t}\n\treturn blocks\n}\n\n// Execute takes performs the execution of the block's command\n// by passing in the block's input as stdin and sets the block output\nfunc (b *Block) Execute() {\n\tc := strings.Split(b.Command, \" \")\n\tcmd := exec.Command(c[0], c[1:]...)\n\tstdin, err := cmd.StdinPipe()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tgo func() {\n\t\tdefer stdin.Close()\n\t\t_, _ = io.WriteString(stdin, b.Input)\n\t}()\n\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tb.Output = string(out)\n}\n\n// Pre processes the markdown content by executing the commands necessary and\n// returns the new processed content\nfunc Pre(content string) string {\n\tblocks := Parse(content)\n\n\tif len(blocks) <= 0 {\n\t\treturn content\n\t}\n\n\tfor _, block := range blocks {\n\t\t// TODO: Use goroutines, if possible\n\t\tblock.Execute()\n\n\t\t// If multiple blocks have the same Raw value The will _likely_ have the\n\t\t// same Output value so we can probably optimize this\n\t\t// There may be edge cases, though, since block execution is not deterministic.\n\t\tcontent = strings.Replace(content, block.Raw, block.Output, 1)\n\t}\n\treturn content\n}\n"
  },
  {
    "path": "internal/process/process_test.go",
    "content": "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 Process\nReplace\n~~~\n\nHello\n\n~~~sd Replace Process\nReplace\nMulti-line input\n~~~\n\n~~~echo -n World\nHello\n~~~\n\n---\n\n# Next Slide\n\nGraphViz Test\n\n~~~graph-easy --as=boxart\ndigraph {\n  A -> B\n}\n~~~\n`\n\n\tgot := Parse(md)\n\twant := []Block{{\n\t\tCommand: \"sd Replace Process\",\n\t\tInput:   \"Replace\",\n\t\tRaw:     \"~~~sd Replace Process\\nReplace\\n~~~\",\n\t}, {\n\t\tCommand: \"sd Replace Process\",\n\t\tInput:   \"Replace\\nMulti-line input\",\n\t\tRaw:     \"~~~sd Replace Process\\nReplace\\nMulti-line input\\n~~~\",\n\t}, {\n\t\tCommand: \"echo -n World\",\n\t\tInput:   \"Hello\",\n\t\tRaw:     \"~~~echo -n World\\nHello\\n~~~\",\n\t}, {\n\t\tCommand: \"graph-easy --as=boxart\",\n\t\tInput:   \"digraph {\\n  A -> B\\n}\",\n\t\tRaw:     \"~~~graph-easy --as=boxart\\ndigraph {\\n  A -> B\\n}\\n~~~\",\n\t}}\n\n\tif !reflect.DeepEqual(got, want) {\n\t\tt.Log(want)\n\t\tt.Log(got)\n\t\tt.Fatal(\"Did not parse blocks correctly\")\n\t}\n}\n"
  },
  {
    "path": "internal/server/middleware.go",
    "content": "package server\n\nimport (\n\t\"fmt\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/ssh\"\n\t\"github.com/charmbracelet/wish\"\n\tbm \"github.com/charmbracelet/wish/bubbletea\"\n\t\"github.com/muesli/termenv\"\n)\n\nfunc slidesMiddleware(srv *Server) wish.Middleware {\n\tnewProg := func(m tea.Model, opts ...tea.ProgramOption) *tea.Program {\n\t\tp := tea.NewProgram(m, opts...)\n\t\treturn p\n\t}\n\tteaHandler := func(s ssh.Session) *tea.Program {\n\t\t_, _, active := s.Pty()\n\t\tif !active {\n\t\t\tfmt.Println(\"no active terminal, skipping\")\n\t\t\terr := s.Exit(1)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Println(\"Error exiting session\")\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\treturn newProg(srv.presentation, tea.WithInput(s), tea.WithOutput(s), tea.WithAltScreen())\n\t}\n\treturn bm.MiddlewareWithProgramHandler(teaHandler, termenv.ANSI256)\n}\n"
  },
  {
    "path": "internal/server/server.go",
    "content": "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.com/maaslalani/slides/internal/model\"\n)\n\n// Server is the server for hosting this presentation.\ntype Server struct {\n\thost         string\n\tport         int\n\tsrv          *ssh.Server\n\tpresentation model.Model\n}\n\n// NewServer creates a new server.\nfunc NewServer(keyPath, host string, port int, presentation model.Model) (*Server, error) {\n\ts := &Server{\n\t\thost:         host,\n\t\tport:         port,\n\t\tpresentation: presentation,\n\t}\n\tsrv, err := wish.NewServer(\n\t\twish.WithHostKeyPath(keyPath),\n\t\twish.WithAddress(fmt.Sprintf(\"%s:%d\", host, port)),\n\t\twish.WithMiddleware(\n\t\t\tslidesMiddleware(s),\n\t\t),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ts.srv = srv\n\treturn s, nil\n}\n\n// Start starts the ssh server.\nfunc (s *Server) Start() error {\n\treturn s.srv.ListenAndServe()\n}\n\n// Shutdown shuts down the server.\nfunc (s *Server) Shutdown(ctx context.Context) error {\n\treturn s.srv.Shutdown(ctx)\n}\n"
  },
  {
    "path": "main.go",
    "content": "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/slides/internal/cmd\"\n\t\"github.com/maaslalani/slides/internal/model\"\n\t\"github.com/maaslalani/slides/internal/navigation\"\n\t\"github.com/muesli/coral\"\n)\n\nvar (\n\trootCmd = &coral.Command{\n\t\tUse:   \"slides <file.md>\",\n\t\tShort: \"Terminal based presentation tool\",\n\t\tArgs:  coral.ArbitraryArgs,\n\t\tRunE: func(cmd *coral.Command, args []string) error {\n\t\t\tvar err error\n\t\t\tvar fileName string\n\n\t\t\tif len(args) > 0 {\n\t\t\t\tfileName = args[0]\n\t\t\t}\n\n\t\t\tpresentation := model.Model{\n\t\t\t\tPage:     0,\n\t\t\t\tDate:     time.Now().Format(\"2006-01-02\"),\n\t\t\t\tFileName: fileName,\n\t\t\t\tSearch:   navigation.NewSearch(),\n\t\t\t}\n\t\t\terr = presentation.Load()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tp := tea.NewProgram(presentation, tea.WithAltScreen())\n\t\t\t_, err = p.Run()\n\t\t\treturn err\n\t\t},\n\t}\n)\n\nfunc init() {\n\trootCmd.AddCommand(\n\t\tcmd.ServeCmd,\n\t)\n\trootCmd.CompletionOptions.DisableDefaultCmd = true\n}\n\nfunc main() {\n\tif err := rootCmd.Execute(); err != nil {\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "snap/snapcraft.yaml",
    "content": "name: slides\nadopt-info: slides\nsummary: Slides in your terminal.\ndescription: |\n  Slides in your terminal.\n  \n  Usage:\n    slides <file.md> [flags]\n\n  Flags:\n    -h, --help   help for slides\n    \nlicense: MIT\n\nbase: core22\ngrade: stable \nconfinement: strict\ncompression: lzo\n\narchitectures:\n  - build-on: amd64\n  - build-on: arm64\n  - build-on: armhf\n  - build-on: ppc64el\n  - build-on: s390x\n  \nassumes:\n  - command-chain\n  \napps:\n  slides:\n    command: bin/slides\n    command-chain: \n      - bin/homeishome-launch     \n    plugs:\n      - home\n      - ssh-keys\n      - ssh-public-keys\n      - network\n      - network-bind\n      \nparts:\n  slides:\n    source: https://github.com/maaslalani/slides\n    source-type: git\n    plugin: go\n    build-snaps:\n      - go\n      \n    override-pull: |\n      snapcraftctl pull\n      snapcraftctl set-version \"$(git describe --tags | sed 's/^v//' | cut -d \"-\" -f1)\"    \n\n  homeishome-launch:\n    plugin: nil\n    stage-snaps:\n      - homeishome-launch     \n"
  },
  {
    "path": "styles/styles.go",
    "content": "// 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\"strings\"\n\n\t\"github.com/charmbracelet/glamour\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/muesli/termenv\"\n)\n\nconst (\n\tsalmon = lipgloss.Color(\"#E8B4BC\")\n)\n\nvar (\n\t// Author is the style for the author text in the bottom-left corner of the\n\t// presentation.\n\tAuthor = lipgloss.NewStyle().Foreground(salmon).Align(lipgloss.Left).MarginLeft(2)\n\t// Date is the style for the date text in the bottom-left corner of the\n\t// presentation.\n\tDate = lipgloss.NewStyle().Faint(true).Align(lipgloss.Left).Margin(0, 1)\n\t// Page is the style for the pagination progress information text in the\n\t// bottom-right corner of the presentation.\n\tPage = lipgloss.NewStyle().Foreground(salmon).Align(lipgloss.Right).MarginRight(3)\n\t// Slide is the style for the slide.\n\tSlide = lipgloss.NewStyle().Padding(1)\n\t// Status is the style for the status bar at the bottom of the\n\t// presentation.\n\tStatus = lipgloss.NewStyle().Padding(1)\n\t// Search is the style for the search input at the bottom-left corner of\n\t// the screen when searching is active.\n\tSearch = lipgloss.NewStyle().Faint(true).Align(lipgloss.Left).MarginLeft(2)\n)\n\nvar (\n\t// DefaultTheme is the default theme for the presentation.\n\t//go:embed theme.json\n\tDefaultTheme []byte\n)\n\n// JoinHorizontal joins two strings horizontally and fills the space in-between.\nfunc JoinHorizontal(left, right string, width int) string {\n\tw := width - lipgloss.Width(right)\n\treturn lipgloss.PlaceHorizontal(w, lipgloss.Left, left) + right\n}\n\n// JoinVertical joins two strings vertically and fills the space in-between.\nfunc JoinVertical(top, bottom string, height int) string {\n\th := height - lipgloss.Height(bottom)\n\treturn lipgloss.PlaceVertical(h, lipgloss.Top, top) + bottom\n}\n\n// SelectTheme picks a glamour style config based\n// on the theme provided in the markdown header\nfunc SelectTheme(theme string) glamour.TermRendererOption {\n\tswitch theme {\n\tcase \"ascii\":\n\t\treturn glamour.WithStyles(glamour.ASCIIStyleConfig)\n\tcase \"light\":\n\t\treturn glamour.WithStyles(glamour.LightStyleConfig)\n\tcase \"dark\":\n\t\treturn glamour.WithStyles(glamour.DarkStyleConfig)\n\tcase \"notty\":\n\t\treturn glamour.WithStyles(glamour.NoTTYStyleConfig)\n\tdefault:\n\t\tvar themeReader io.Reader\n\t\tvar err error\n\t\tif strings.HasPrefix(theme, \"http\") {\n\t\t\tvar resp *http.Response\n\t\t\tresp, err = http.Get(theme)\n\t\t\tif err != nil {\n\t\t\t\treturn getDefaultTheme()\n\t\t\t}\n\t\t\tdefer resp.Body.Close()\n\t\t\tthemeReader = resp.Body\n\t\t} else {\n\t\t\tfile, err := os.Open(theme)\n\t\t\tif err != nil {\n\t\t\t\treturn getDefaultTheme()\n\t\t\t}\n\t\t\tdefer file.Close()\n\t\t\tthemeReader = file\n\t\t}\n\t\tbytes, err := io.ReadAll(themeReader)\n\t\tif err == nil {\n\t\t\treturn glamour.WithStylesFromJSONBytes(bytes)\n\t\t}\n\t\t// Should log a warning so the user knows we failed to read their theme file\n\t\treturn getDefaultTheme()\n\t}\n}\n\nfunc getDefaultTheme() glamour.TermRendererOption {\n\tif termenv.EnvNoColor() {\n\t\treturn glamour.WithStyles(glamour.NoTTYStyleConfig)\n\t}\n\n\tif !termenv.HasDarkBackground() {\n\t\treturn glamour.WithStyles(glamour.LightStyleConfig)\n\t}\n\n\treturn glamour.WithStylesFromJSONBytes(DefaultTheme)\n}\n"
  },
  {
    "path": "styles/styles_test.go",
    "content": "package styles_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/charmbracelet/glamour\"\n\t\"github.com/charmbracelet/glamour/ansi\"\n\t\"github.com/maaslalani/slides/styles\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSelectTheme(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\ttheme   string\n\t\twant    ansi.StyleConfig\n\t\twantErr bool\n\t}{\n\t\t{name: \"Select dark theme\", theme: \"dark\", want: glamour.DarkStyleConfig, wantErr: false},\n\t\t{name: \"Select light theme\", theme: \"light\", want: glamour.LightStyleConfig, wantErr: false},\n\t\t{name: \"Select ascii theme\", theme: \"ascii\", want: glamour.ASCIIStyleConfig, wantErr: false},\n\t\t{name: \"Select notty theme\", theme: \"notty\", want: glamour.NoTTYStyleConfig, wantErr: false},\n\t\t{name: \"Select theme with error\", theme: \"notty\", want: glamour.DarkStyleConfig, wantErr: true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Execute the theme selection and ensure\n\t\t\t// it returns a non-nil theme\n\t\t\tselectedTheme := styles.SelectTheme(tt.theme)\n\t\t\tassert.NotNil(t, selectedTheme)\n\n\t\t\t// Initialize renderers to compare output\n\t\t\tgotRenderer, _ := glamour.NewTermRenderer(selectedTheme)\n\t\t\twantRenderer, _ := glamour.NewTermRenderer(glamour.WithStyles(tt.want))\n\n\t\t\t// Render a the same string with two different\n\t\t\t// renderers\n\t\t\tgotOutput, _ := gotRenderer.Render(tt.name)\n\t\t\twantOutput, _ := wantRenderer.Render(tt.name)\n\n\t\t\t// Inject exception to ensure a style that doesn't match\n\t\t\t// it's associated string\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.NotEqual(t, wantOutput, gotOutput)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Ensure they both match\n\t\t\tassert.Equal(t, wantOutput, gotOutput)\n\t\t})\n\t}\n}\n\nfunc TestSelectTheme_file(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\ttheme      string\n\t\tfileExists bool\n\t}{\n\t\t{name: \"Select custom theme json\", theme: \"./theme.json\", fileExists: true},\n\t\t{name: \"Use an invalid filepath\", theme: \"./someinvalidfile.toml\", fileExists: false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Successfully return a theme if a file exists\n\t\t\tassert.NotNil(t, styles.SelectTheme(tt.theme))\n\n\t\t\t// Successfully return a theme if a file doesn't exist\n\t\t\tif !tt.fileExists {\n\t\t\t\tassert.NotNil(t, styles.SelectTheme(tt.theme))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "styles/theme.json",
    "content": "{\n  \"document\": {\n    \"block_prefix\": \"\\n\",\n    \"block_suffix\": \"\\n\",\n    \"color\": \"252\",\n    \"margin\": 2\n  },\n  \"block_quote\": {\n    \"indent\": 1,\n    \"indent_token\": \"│ \"\n  },\n  \"paragraph\": {},\n  \"list\": {\n    \"level_indent\": 2\n  },\n  \"heading\": {\n    \"block_suffix\": \"\\n\",\n    \"color\": \"39\",\n    \"bold\": true\n  },\n  \"h1\": {\n    \"prefix\": \"██ \",\n    \"suffix\": \" \",\n    \"color\": \"#9fc\",\n    \"bold\": true\n  },\n  \"h2\": {\n    \"prefix\": \"▓▓▓ \",\n    \"color\": \"#1cc\"\n  },\n  \"h3\": {\n    \"prefix\": \"▒▒▒▒ \",\n    \"color\": \"#29c\"\n  },\n  \"h4\": {\n    \"color\": \"#559\",\n    \"prefix\": \"░░░░░ \"\n  },\n  \"h5\": {},\n  \"h6\": {},\n  \"text\": {},\n  \"strikethrough\": {\n    \"crossed_out\": true\n  },\n  \"emph\": {\n    \"italic\": true\n  },\n  \"strong\": {\n    \"bold\": true\n  },\n  \"hr\": {\n    \"color\": \"240\",\n    \"format\": \"\\n--------\\n\"\n  },\n  \"item\": {\n    \"block_prefix\": \"• \"\n  },\n  \"enumeration\": {\n    \"block_prefix\": \". \"\n  },\n  \"task\": {\n    \"ticked\": \"[✓] \",\n    \"unticked\": \"[ ] \"\n  },\n  \"link\": {\n    \"color\": \"30\",\n    \"underline\": true\n  },\n  \"link_text\": {\n    \"color\": \"35\",\n    \"bold\": true\n  },\n  \"image\": {\n    \"color\": \"212\",\n    \"underline\": true\n  },\n  \"image_text\": {\n    \"color\": \"243\",\n    \"format\": \"Image: {{.text}} →\"\n  },\n  \"code\": {\n    \"prefix\": \" \",\n    \"suffix\": \" \",\n    \"color\": \"203\",\n    \"background_color\": \"236\"\n  },\n  \"code_block\": {\n    \"theme\": \"dracula\",\n    \"margin\": 2\n  },\n  \"table\": {\n    \"center_separator\": \"┼\",\n    \"column_separator\": \"│\",\n    \"row_separator\": \"─\"\n  },\n  \"definition_list\": {},\n  \"definition_term\": {},\n  \"definition_description\": {\n    \"block_prefix\": \"\\n🠶 \"\n  },\n  \"html_block\": {},\n  \"html_span\": {}\n}\n"
  }
]