mirror of
https://github.com/mfontanini/presenterm.git
synced 2026-09-01 23:33:21 -06:00
Merge branch 'master' into feat/auto-theme
This commit is contained in:
commit
4d15779f03
6
.github/workflows/merge.yaml
vendored
6
.github/workflows/merge.yaml
vendored
@ -83,4 +83,8 @@ jobs:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
- name: Validate config file schema
|
||||
run: ./scripts/validate-config-file-schema.sh
|
||||
run: |
|
||||
if ! ./scripts/validate-config-file-schema.sh; then
|
||||
echo -e "\033[31;1mRun ./scripts/generate-config-file-schema.sh to regenerate JSON schema"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
157
Cargo.lock
generated
157
Cargo.lock
generated
@ -100,6 +100,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
@ -143,6 +149,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.51"
|
||||
@ -239,7 +251,7 @@ version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"crossterm_winapi",
|
||||
"document-features",
|
||||
"mio",
|
||||
@ -299,10 +311,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "downcast"
|
||||
version = "0.11.0"
|
||||
name = "downcast-rs"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
|
||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
@ -353,6 +365,17 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filedescriptor"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"thiserror 1.0.69",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.4"
|
||||
@ -480,6 +503,12 @@ version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
@ -492,7 +521,7 @@ version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@ -609,6 +638,18 @@ dependencies = [
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
@ -651,7 +692,7 @@ version = "6.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"onig_sys",
|
||||
@ -731,13 +772,34 @@ version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-pty"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 1.3.2",
|
||||
"downcast-rs",
|
||||
"filedescriptor",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"nix",
|
||||
"serial2",
|
||||
"shared_library",
|
||||
"shell-words",
|
||||
"winapi",
|
||||
"winreg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
@ -790,6 +852,7 @@ dependencies = [
|
||||
"merge-struct",
|
||||
"once_cell",
|
||||
"os_pipe",
|
||||
"portable-pty",
|
||||
"rstest",
|
||||
"schemars",
|
||||
"serde",
|
||||
@ -801,9 +864,10 @@ dependencies = [
|
||||
"syntect",
|
||||
"tempfile",
|
||||
"termbg",
|
||||
"thiserror",
|
||||
"thiserror 2.0.17",
|
||||
"tl",
|
||||
"unicode-width",
|
||||
"vt100",
|
||||
"vte",
|
||||
]
|
||||
|
||||
@ -849,7 +913,7 @@ version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -860,7 +924,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -952,7 +1016,7 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.11.0",
|
||||
@ -1077,6 +1141,33 @@ dependencies = [
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial2"
|
||||
version = "0.2.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cc76fa68e25e771492ca1e3c53d447ef0be3093e05cd3b47f4b712ba10c6f3c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_library"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@ -1218,7 +1309,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"thiserror 2.0.17",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@ -1264,13 +1355,33 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
"thiserror-impl 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1383,6 +1494,17 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "vt100"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "054ff75fb8fa83e609e685106df4faeffdf3a735d3c74ebce97ec557d5d36fd9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"unicode-width",
|
||||
"vte",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vte"
|
||||
version = "0.15.0"
|
||||
@ -1608,6 +1730,15 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.4.12"
|
||||
|
||||
@ -23,6 +23,7 @@ sixel-rs = { version = "0.4.1", optional = true }
|
||||
merge-struct = "0.1.0"
|
||||
itertools = "0.14"
|
||||
once_cell = "1.19"
|
||||
portable-pty = "0.9"
|
||||
schemars = { version = "0.8", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_yaml = "0.9"
|
||||
@ -38,6 +39,7 @@ os_pipe = "1.1.5"
|
||||
libc = "0.2"
|
||||
vte = "0.15"
|
||||
termbg = "0.6.2"
|
||||
vt100 = "0.16"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = { version = "0.26", default-features = false }
|
||||
|
||||
@ -853,6 +853,7 @@
|
||||
"Clojure",
|
||||
"Cpp",
|
||||
"Css",
|
||||
"Dart",
|
||||
"D2",
|
||||
"DLang",
|
||||
"Diff",
|
||||
@ -901,6 +902,7 @@
|
||||
"Terraform",
|
||||
"Toml",
|
||||
"TypeScript",
|
||||
"TypeScriptReact",
|
||||
"Typst",
|
||||
"Xml",
|
||||
"Yaml",
|
||||
|
||||
@ -65,7 +65,7 @@ When you pass in this flag, _presenterm_ will:
|
||||
that the `+validate` flag is a special one that doesn't make a snippet executable but still validates it by running it
|
||||
during development.
|
||||
* Report an error if any of the snippets returns an exit code other than 0.
|
||||
* Re-run all snippets `+exec` and `+exec_repalce` snippets every time the presentation is reloaded.
|
||||
* Re-run all snippets `+exec` and `+exec_replace` snippets every time the presentation is reloaded.
|
||||
|
||||
In case you expect a snippet to return an exit code other than 0, you can use the `+expect:failure` flag. This will
|
||||
cause _presenterm_ to display an error if the snippet does not fail.
|
||||
|
||||
105
executors.yaml
105
executors.yaml
@ -7,18 +7,31 @@ bash:
|
||||
c++:
|
||||
filename: snippet.cpp
|
||||
commands:
|
||||
- ["g++", "-std=c++20", "-fdiagnostics-color=always", "$pwd/snippet.cpp", "-o", "$pwd/snippet"]
|
||||
- [
|
||||
"g++",
|
||||
"-std=c++20",
|
||||
"-fdiagnostics-color=always",
|
||||
"$pwd/snippet.cpp",
|
||||
"-o",
|
||||
"$pwd/snippet",
|
||||
]
|
||||
- ["$pwd/snippet"]
|
||||
hidden_line_prefix: "/// "
|
||||
c:
|
||||
filename: snippet.c
|
||||
commands:
|
||||
- ["gcc", "$pwd/snippet.c", "-fdiagnostics-color=always", "-o", "$pwd/snippet"]
|
||||
- [
|
||||
"gcc",
|
||||
"$pwd/snippet.c",
|
||||
"-fdiagnostics-color=always",
|
||||
"-o",
|
||||
"$pwd/snippet",
|
||||
]
|
||||
- ["$pwd/snippet"]
|
||||
hidden_line_prefix: "/// "
|
||||
elixir:
|
||||
filename: snippet.exs
|
||||
commands:
|
||||
commands:
|
||||
- ["elixir", "$pwd/snippet.exs"]
|
||||
hidden_line_prefix: "## "
|
||||
fish:
|
||||
@ -44,9 +57,84 @@ java:
|
||||
hidden_line_prefix: "/// "
|
||||
js:
|
||||
filename: snippet.js
|
||||
environment:
|
||||
FORCE_COLOR: "true"
|
||||
commands:
|
||||
- ["node", "$pwd/snippet.js"]
|
||||
hidden_line_prefix: "/// "
|
||||
alternative:
|
||||
deno:
|
||||
filename: "snippet.js"
|
||||
environment:
|
||||
FORCE_COLOR: "true"
|
||||
commands:
|
||||
- ["deno", "run", "-A", "$pwd/snippet.js"]
|
||||
bun:
|
||||
filename: "snippet.js"
|
||||
environment:
|
||||
FORCE_COLOR: "true"
|
||||
commands:
|
||||
- ["bun", "run", "$pwd/snippet.js"]
|
||||
jsx:
|
||||
filename: snippet.jsx
|
||||
environment:
|
||||
FORCE_COLOR: "true"
|
||||
commands:
|
||||
- ["node", "$pwd/snippet.jsx"]
|
||||
hidden_line_prefix: "/// "
|
||||
alternative:
|
||||
deno:
|
||||
filename: "snippet.jsx"
|
||||
environment:
|
||||
FORCE_COLOR: "true"
|
||||
commands:
|
||||
- ["deno", "run", "-A", "$pwd/snippet.jsx"]
|
||||
bun:
|
||||
filename: "snippet.jsx"
|
||||
environment:
|
||||
FORCE_COLOR: "true"
|
||||
commands:
|
||||
- ["bun", "run", "$pwd/snippet.jsx"]
|
||||
ts:
|
||||
filename: snippet.ts
|
||||
environment:
|
||||
FORCE_COLOR: "true"
|
||||
commands:
|
||||
- ["node", "$pwd/snippet.ts"]
|
||||
hidden_line_prefix: "/// "
|
||||
alternative:
|
||||
deno:
|
||||
filename: "snippet.ts"
|
||||
environment:
|
||||
FORCE_COLOR: "true"
|
||||
commands:
|
||||
- ["deno", "run", "-A", "$pwd/snippet.ts"]
|
||||
bun:
|
||||
filename: "snippet.ts"
|
||||
environment:
|
||||
FORCE_COLOR: "true"
|
||||
commands:
|
||||
- ["bun", "run", "$pwd/snippet.ts"]
|
||||
tsx:
|
||||
filename: snippet.tsx
|
||||
environment:
|
||||
FORCE_COLOR: "true"
|
||||
commands:
|
||||
- ["node", "$pwd/snippet.tsx"]
|
||||
hidden_line_prefix: "/// "
|
||||
alternative:
|
||||
deno:
|
||||
filename: "snippet.tsx"
|
||||
environment:
|
||||
FORCE_COLOR: "true"
|
||||
commands:
|
||||
- ["deno", "run", "-A", "$pwd/snippet.tsx"]
|
||||
bun:
|
||||
filename: "snippet.tsx"
|
||||
environment:
|
||||
FORCE_COLOR: "true"
|
||||
commands:
|
||||
- ["bun", "run", "$pwd/snippet.tsx"]
|
||||
julia:
|
||||
filename: snippet.jl
|
||||
commands:
|
||||
@ -107,7 +195,16 @@ rust-script:
|
||||
rust:
|
||||
filename: snippet.rs
|
||||
commands:
|
||||
- ["rustc", "--crate-name", "presenterm_snippet", "$pwd/snippet.rs", "-o", "$pwd/snippet", "--color", "always"]
|
||||
- [
|
||||
"rustc",
|
||||
"--crate-name",
|
||||
"presenterm_snippet",
|
||||
"$pwd/snippet.rs",
|
||||
"-o",
|
||||
"$pwd/snippet",
|
||||
"--color",
|
||||
"always",
|
||||
]
|
||||
- ["$pwd/snippet"]
|
||||
hidden_line_prefix: "# "
|
||||
alternative:
|
||||
|
||||
17
scripts/generate-config-file-schema.sh
Executable file
17
scripts/generate-config-file-schema.sh
Executable file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=$(dirname "$0")
|
||||
root_dir="${script_dir}/../"
|
||||
|
||||
current_schema=$(mktemp)
|
||||
docker run \
|
||||
--rm \
|
||||
-v "${root_dir}:/tmp/workspace" \
|
||||
-w "/tmp/workspace" \
|
||||
rust:1.86 \
|
||||
cargo run --features json-schema -q -- --generate-config-file-schema >"${current_schema}"
|
||||
|
||||
cp "$current_schema" "${root_dir}/config-file-schema.json"
|
||||
rm "$current_schema"
|
||||
@ -1,8 +1,8 @@
|
||||
//! Code execution.
|
||||
|
||||
use super::snippet::{SnippetExecutorSpec, SnippetRepr};
|
||||
use super::snippet::SnippetExecutorSpec;
|
||||
use crate::{
|
||||
code::snippet::{Snippet, SnippetLanguage},
|
||||
code::snippet::{Snippet, SnippetExecution, SnippetLanguage, SnippetRepr},
|
||||
config::{LanguageSnippetExecutionConfig, SnippetExecutorConfig},
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
@ -115,8 +115,8 @@ impl LanguageSnippetExecutor {
|
||||
pub(crate) fn execute_async(&self, snippet: &Snippet) -> Result<ExecutionHandle, CodeExecuteError> {
|
||||
let script_dir = self.write_snippet(snippet)?;
|
||||
let state: Arc<Mutex<ExecutionState>> = Default::default();
|
||||
let output_type = match snippet.attributes.representation {
|
||||
SnippetRepr::Image => OutputType::Binary,
|
||||
let output_type = match &snippet.attributes.execution {
|
||||
SnippetExecution::Exec(args) if matches!(args.repr, SnippetRepr::Image) => OutputType::Binary,
|
||||
_ => OutputType::Lines,
|
||||
};
|
||||
let reader_handle = CommandsRunner::spawn(
|
||||
@ -135,28 +135,58 @@ impl LanguageSnippetExecutor {
|
||||
pub(crate) fn execute_sync(&self, snippet: &Snippet) -> Result<(), CodeExecuteError> {
|
||||
let script_dir = self.write_snippet(snippet)?;
|
||||
let script_dir_path = script_dir.path().to_string_lossy();
|
||||
for mut commands in self.config.commands.clone() {
|
||||
for command in &mut commands {
|
||||
*command = command.replace("$pwd", &script_dir_path);
|
||||
}
|
||||
let (command, args) = commands.split_first().expect("no commands");
|
||||
let child = process::Command::new(command)
|
||||
.args(args)
|
||||
.envs(&self.config.environment)
|
||||
.current_dir(&self.cwd)
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| CodeExecuteError::SpawnProcess(command.clone(), e))?;
|
||||
|
||||
let output = child.wait_with_output().map_err(CodeExecuteError::Waiting)?;
|
||||
if !output.status.success() {
|
||||
let error = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
return Err(CodeExecuteError::Running(error));
|
||||
}
|
||||
for commands in self.config.commands.clone() {
|
||||
self.execute_command(commands, &script_dir_path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates the necessary context to run this snippet in a PTY.
|
||||
pub(crate) fn pty_execution_context(&self, snippet: &Snippet) -> Result<PtySnippetContext, CodeExecuteError> {
|
||||
let script_dir = self.write_snippet(snippet)?;
|
||||
let script_dir_path = script_dir.path().to_string_lossy();
|
||||
|
||||
// Run the first N-1 commands normally and assume the last one is the one that actually
|
||||
// invokes the thing (e.g. rust snippet compilation happens here, snippet execution in PTY)
|
||||
for commands in self.config.commands.iter().take(self.config.commands.len() - 1).cloned() {
|
||||
self.execute_command(commands, &script_dir_path)?;
|
||||
}
|
||||
let mut commands = self.config.commands.last().cloned().unwrap();
|
||||
for command in &mut commands {
|
||||
*command = command.replace("$pwd", &script_dir_path);
|
||||
}
|
||||
let (command, args) = commands.split_first().expect("no commands");
|
||||
let mut command = portable_pty::CommandBuilder::new(command);
|
||||
command.args(args);
|
||||
command.cwd(&self.cwd);
|
||||
for (key, value) in &self.config.environment {
|
||||
command.env(key, value);
|
||||
}
|
||||
Ok(PtySnippetContext { command, _temp: script_dir })
|
||||
}
|
||||
|
||||
fn execute_command(&self, mut commands: Vec<String>, script_dir_path: &str) -> Result<(), CodeExecuteError> {
|
||||
for command in &mut commands {
|
||||
*command = command.replace("$pwd", script_dir_path);
|
||||
}
|
||||
let (command, args) = commands.split_first().expect("no commands");
|
||||
let child = process::Command::new(command)
|
||||
.args(args)
|
||||
.envs(&self.config.environment)
|
||||
.current_dir(&self.cwd)
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| CodeExecuteError::SpawnProcess(command.clone(), e))?;
|
||||
|
||||
let output = child.wait_with_output().map_err(CodeExecuteError::Waiting)?;
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
Err(CodeExecuteError::Running(error))
|
||||
}
|
||||
}
|
||||
|
||||
fn write_snippet(&self, snippet: &Snippet) -> Result<TempDir, CodeExecuteError> {
|
||||
let hide_prefix = self.hidden_line_prefix.as_deref();
|
||||
let code = snippet.executable_contents(hide_prefix);
|
||||
@ -169,6 +199,11 @@ impl LanguageSnippetExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PtySnippetContext {
|
||||
pub(crate) command: portable_pty::CommandBuilder,
|
||||
_temp: TempDir,
|
||||
}
|
||||
|
||||
/// An invalid executor was found.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("invalid snippet execution for '{0:?}': {1}")]
|
||||
@ -328,7 +363,7 @@ pub(crate) struct ExecutionState {
|
||||
}
|
||||
|
||||
/// The status of a process.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub(crate) enum ProcessStatus {
|
||||
#[default]
|
||||
Running,
|
||||
@ -346,7 +381,7 @@ impl ProcessStatus {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::code::snippet::{SnippetAttributes, SnippetExec};
|
||||
use crate::code::snippet::{SnippetAttributes, SnippetExecution};
|
||||
|
||||
#[test]
|
||||
fn shell_code_execution() {
|
||||
@ -357,7 +392,10 @@ echo 'bye'"
|
||||
let snippet = Snippet {
|
||||
contents,
|
||||
language: SnippetLanguage::Shell,
|
||||
attributes: SnippetAttributes { execution: SnippetExec::Exec(Default::default()), ..Default::default() },
|
||||
attributes: SnippetAttributes {
|
||||
execution: SnippetExecution::Exec(Default::default()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
let executor = SnippetExecutor::default().language_executor(&snippet.language, &Default::default()).unwrap();
|
||||
let handle = executor.execute_async(&snippet).expect("execution failed");
|
||||
@ -382,7 +420,10 @@ echo 'hello world'
|
||||
let snippet = Snippet {
|
||||
contents,
|
||||
language: SnippetLanguage::Shell,
|
||||
attributes: SnippetAttributes { execution: SnippetExec::Exec(Default::default()), ..Default::default() },
|
||||
attributes: SnippetAttributes {
|
||||
execution: SnippetExecution::Exec(Default::default()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
let executor = SnippetExecutor::default().language_executor(&snippet.language, &Default::default()).unwrap();
|
||||
let handle = executor.execute_async(&snippet).expect("execution failed");
|
||||
@ -408,7 +449,10 @@ echo 'hello world'
|
||||
let snippet = Snippet {
|
||||
contents,
|
||||
language: SnippetLanguage::Shell,
|
||||
attributes: SnippetAttributes { execution: SnippetExec::Exec(Default::default()), ..Default::default() },
|
||||
attributes: SnippetAttributes {
|
||||
execution: SnippetExecution::Exec(Default::default()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
let executor = SnippetExecutor::default().language_executor(&snippet.language, &Default::default()).unwrap();
|
||||
let handle = executor.execute_async(&snippet).expect("execution failed");
|
||||
|
||||
@ -115,6 +115,7 @@ impl SnippetHighlighter {
|
||||
Cpp => "cpp",
|
||||
Crontab => "crontab",
|
||||
Css => "css",
|
||||
Dart => "dart",
|
||||
D2 => "txt",
|
||||
DLang => "d",
|
||||
Diff => "diff",
|
||||
@ -163,6 +164,7 @@ impl SnippetHighlighter {
|
||||
Terraform => "tf",
|
||||
Toml => "toml",
|
||||
TypeScript => "ts",
|
||||
TypeScriptReact => "tsx",
|
||||
Typst => "txt",
|
||||
// default to plain text so we get the same look&feel
|
||||
Unknown(_) => "txt",
|
||||
|
||||
@ -209,7 +209,7 @@ impl SnippetParser {
|
||||
fn parse_block_info(input: &str) -> ParseResult<(SnippetLanguage, SnippetAttributes)> {
|
||||
let (language, input) = Self::parse_language(input);
|
||||
let attributes = Self::parse_attributes(input)?;
|
||||
if attributes.width.is_some() && !matches!(attributes.representation, SnippetRepr::Render) {
|
||||
if attributes.width.is_some() && !matches!(attributes.execution, SnippetExecution::Render) {
|
||||
return Err(SnippetBlockParseError::NotRenderSnippet("width"));
|
||||
}
|
||||
Ok((language, attributes))
|
||||
@ -233,38 +233,58 @@ impl SnippetParser {
|
||||
}
|
||||
use SnippetAttribute::*;
|
||||
match attribute {
|
||||
ExecReplace(_) | Image | Render if attributes.representation != SnippetRepr::Snippet => {
|
||||
return Err(SnippetBlockParseError::MultipleRepresentation);
|
||||
}
|
||||
LineNumbers => attributes.line_numbers = true,
|
||||
Exec(spec) => {
|
||||
if !matches!(attributes.execution, SnippetExec::AcquireTerminal(_)) {
|
||||
attributes.execution = SnippetExec::Exec(spec);
|
||||
}
|
||||
attributes.execution = attributes
|
||||
.execution
|
||||
.try_merge(SnippetExecution::Exec(SnippetExecArgs { spec, ..Default::default() }))?;
|
||||
}
|
||||
AutoExec(spec) => {
|
||||
if !matches!(attributes.execution, SnippetExec::AcquireTerminal(_)) {
|
||||
attributes.execution = SnippetExec::AutoExec(spec);
|
||||
}
|
||||
attributes.execution = attributes.execution.try_merge(SnippetExecution::Exec(SnippetExecArgs {
|
||||
spec,
|
||||
auto: true,
|
||||
..Default::default()
|
||||
}))?;
|
||||
}
|
||||
ExecPty(spec, args) => {
|
||||
attributes.execution = attributes.execution.try_merge(SnippetExecution::Exec(SnippetExecArgs {
|
||||
spec,
|
||||
pty: Some(args),
|
||||
..Default::default()
|
||||
}))?;
|
||||
}
|
||||
ExecReplace(spec) => {
|
||||
attributes.representation = SnippetRepr::ExecReplace;
|
||||
attributes.execution = SnippetExec::Exec(spec);
|
||||
attributes.execution = attributes.execution.try_merge(SnippetExecution::Exec(SnippetExecArgs {
|
||||
spec,
|
||||
repr: SnippetRepr::ExecReplace,
|
||||
..Default::default()
|
||||
}))?;
|
||||
}
|
||||
Id(id) => {
|
||||
attributes.id = Some(id);
|
||||
}
|
||||
Validate(spec) => {
|
||||
if matches!(attributes.execution, SnippetExec::None) {
|
||||
attributes.execution = SnippetExec::Validate(spec);
|
||||
if attributes.validate.is_some() {
|
||||
return Err(SnippetBlockParseError::DuplicateAttribute("+validate"));
|
||||
}
|
||||
attributes.validate = Some(spec);
|
||||
}
|
||||
Image => {
|
||||
attributes.representation = SnippetRepr::Image;
|
||||
attributes.execution = SnippetExec::Exec(Default::default());
|
||||
attributes.execution = attributes.execution.try_merge(SnippetExecution::Exec(SnippetExecArgs {
|
||||
repr: SnippetRepr::Image,
|
||||
..Default::default()
|
||||
}))?;
|
||||
}
|
||||
Render => {
|
||||
attributes.execution = attributes.execution.try_merge(SnippetExecution::Render)?;
|
||||
}
|
||||
AcquireTerminal(spec) => {
|
||||
attributes.execution = attributes.execution.try_merge(SnippetExecution::Exec(SnippetExecArgs {
|
||||
spec,
|
||||
repr: SnippetRepr::AcquireTerminal,
|
||||
..Default::default()
|
||||
}))?;
|
||||
}
|
||||
Render => attributes.representation = SnippetRepr::Render,
|
||||
AcquireTerminal(spec) => attributes.execution = SnippetExec::AcquireTerminal(spec),
|
||||
NoBackground => attributes.no_background = true,
|
||||
HighlightedLines(lines) => attributes.highlight_groups = lines,
|
||||
Width(width) => attributes.width = Some(width),
|
||||
@ -294,6 +314,7 @@ impl SnippetParser {
|
||||
"render" => SnippetAttribute::Render,
|
||||
"no_background" => SnippetAttribute::NoBackground,
|
||||
"acquire_terminal" => SnippetAttribute::AcquireTerminal(SnippetExecutorSpec::default()),
|
||||
"pty" => SnippetAttribute::ExecPty(SnippetExecutorSpec::default(), Default::default()),
|
||||
other => {
|
||||
let (attribute, parameter) = other
|
||||
.split_once(':')
|
||||
@ -330,6 +351,7 @@ impl SnippetParser {
|
||||
));
|
||||
}
|
||||
},
|
||||
"pty" => SnippetAttribute::ExecPty(SnippetExecutorSpec::default(), parameter.parse()?),
|
||||
_ => return Err(SnippetBlockParseError::InvalidToken(Self::next_identifier(input).into())),
|
||||
}
|
||||
}
|
||||
@ -420,6 +442,9 @@ pub enum SnippetBlockParseError {
|
||||
#[error("invalid width: {0}")]
|
||||
InvalidWidth(PercentParseError),
|
||||
|
||||
#[error("invalid pty args, expected '<columns>:<rows>'")]
|
||||
InvalidPtyArgs,
|
||||
|
||||
#[error("duplicate attribute: {0}")]
|
||||
DuplicateAttribute(&'static str),
|
||||
|
||||
@ -436,6 +461,7 @@ enum SnippetAttribute {
|
||||
Exec(SnippetExecutorSpec),
|
||||
AutoExec(SnippetExecutorSpec),
|
||||
ExecReplace(SnippetExecutorSpec),
|
||||
ExecPty(SnippetExecutorSpec, PtyArgs),
|
||||
Validate(SnippetExecutorSpec),
|
||||
Image,
|
||||
Render,
|
||||
@ -514,6 +540,7 @@ pub enum SnippetLanguage {
|
||||
Clojure,
|
||||
Cpp,
|
||||
Css,
|
||||
Dart,
|
||||
D2,
|
||||
DLang,
|
||||
Diff,
|
||||
@ -562,6 +589,7 @@ pub enum SnippetLanguage {
|
||||
Terraform,
|
||||
Toml,
|
||||
TypeScript,
|
||||
TypeScriptReact,
|
||||
Typst,
|
||||
Unknown(String),
|
||||
Xml,
|
||||
@ -591,6 +619,7 @@ impl FromStr for SnippetLanguage {
|
||||
"clojure" => Clojure,
|
||||
"cpp" | "c++" => Cpp,
|
||||
"css" => Css,
|
||||
"dart" => Dart,
|
||||
"d2" => D2,
|
||||
"d" => DLang,
|
||||
"diff" => Diff,
|
||||
@ -639,6 +668,7 @@ impl FromStr for SnippetLanguage {
|
||||
"terraform" => Terraform,
|
||||
"toml" => Toml,
|
||||
"typescript" | "ts" => TypeScript,
|
||||
"tsx" => TypeScriptReact,
|
||||
"typst" => Typst,
|
||||
"xml" => Xml,
|
||||
"yaml" => Yaml,
|
||||
@ -655,11 +685,8 @@ impl FromStr for SnippetLanguage {
|
||||
/// Attributes for code snippets.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct SnippetAttributes {
|
||||
/// The way the snippet should be represented.
|
||||
pub(crate) representation: SnippetRepr,
|
||||
|
||||
/// The way the snippet should be executed.
|
||||
pub(crate) execution: SnippetExec,
|
||||
/// The way the snippet should be executed, if any.
|
||||
pub(crate) execution: SnippetExecution,
|
||||
|
||||
/// Whether the snippet should show line numbers.
|
||||
pub(crate) line_numbers: bool,
|
||||
@ -675,6 +702,9 @@ pub(crate) struct SnippetAttributes {
|
||||
/// Whether to add no background to a snippet.
|
||||
pub(crate) no_background: bool,
|
||||
|
||||
/// The spec to use to validate this snippet.
|
||||
pub(crate) validate: Option<SnippetExecutorSpec>,
|
||||
|
||||
/// The expected execution result for a snippet.
|
||||
pub(crate) expected_execution_result: ExpectedSnippetExecutionResult,
|
||||
|
||||
@ -685,20 +715,74 @@ pub(crate) struct SnippetAttributes {
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) enum SnippetRepr {
|
||||
#[default]
|
||||
Snippet,
|
||||
SnippetOutput,
|
||||
Image,
|
||||
Render,
|
||||
ExecReplace,
|
||||
AcquireTerminal,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) enum SnippetExec {
|
||||
pub(crate) struct PtyArgs {
|
||||
pub(crate) columns: Option<u16>,
|
||||
pub(crate) rows: Option<u16>,
|
||||
}
|
||||
|
||||
impl FromStr for PtyArgs {
|
||||
type Err = SnippetBlockParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let Some((columns, rows)) = s.split_once(':') else {
|
||||
return Err(SnippetBlockParseError::InvalidPtyArgs);
|
||||
};
|
||||
let columns = columns.parse().map_err(|_| SnippetBlockParseError::InvalidPtyArgs)?;
|
||||
let rows = rows.parse().map_err(|_| SnippetBlockParseError::InvalidPtyArgs)?;
|
||||
Ok(Self { columns: Some(columns), rows: Some(rows) })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct SnippetExecArgs {
|
||||
pub(crate) spec: SnippetExecutorSpec,
|
||||
pub(crate) auto: bool,
|
||||
pub(crate) pty: Option<PtyArgs>,
|
||||
pub(crate) repr: SnippetRepr,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) enum SnippetExecution {
|
||||
#[default]
|
||||
None,
|
||||
Exec(SnippetExecutorSpec),
|
||||
AutoExec(SnippetExecutorSpec),
|
||||
AcquireTerminal(SnippetExecutorSpec),
|
||||
Validate(SnippetExecutorSpec),
|
||||
Render,
|
||||
Exec(SnippetExecArgs),
|
||||
}
|
||||
|
||||
impl SnippetExecution {
|
||||
fn try_merge(self, other: SnippetExecution) -> ParseResult<Self> {
|
||||
match (self, other) {
|
||||
(Self::None, other) => Ok(other),
|
||||
(Self::Render, Self::None) => Ok(Self::Render),
|
||||
(Self::Render, Self::Render) => Err(SnippetBlockParseError::DuplicateAttribute("+render")),
|
||||
(Self::Render, Self::Exec(_)) | (Self::Exec(_), Self::Render) => {
|
||||
Err(SnippetBlockParseError::MultipleRepresentation)
|
||||
}
|
||||
(Self::Exec(mut ours), Self::Exec(theirs)) => {
|
||||
let SnippetExecArgs { spec, auto, pty, repr } = theirs;
|
||||
ours.auto = ours.auto || auto;
|
||||
ours.pty = pty.or(ours.pty);
|
||||
ours.spec = match ours.spec {
|
||||
SnippetExecutorSpec::Default => spec,
|
||||
SnippetExecutorSpec::Alternative(spec) => SnippetExecutorSpec::Alternative(spec),
|
||||
};
|
||||
ours.repr = match (ours.repr, repr) {
|
||||
(SnippetRepr::SnippetOutput, other) => other,
|
||||
(ours, SnippetRepr::SnippetOutput) => ours,
|
||||
_ => return Err(SnippetBlockParseError::MultipleRepresentation),
|
||||
};
|
||||
Ok(Self::Exec(ours))
|
||||
}
|
||||
(Self::Exec(args), Self::None) => Ok(Self::Exec(args)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
@ -796,30 +880,34 @@ mod test {
|
||||
#[test]
|
||||
fn one_attribute() {
|
||||
let attributes = parse_attributes("bash +exec");
|
||||
assert_eq!(attributes.execution, SnippetExec::Exec(Default::default()));
|
||||
assert_eq!(attributes.execution, SnippetExecution::Exec(Default::default()));
|
||||
assert!(!attributes.line_numbers);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_attributes() {
|
||||
let attributes = parse_attributes("bash +exec +line_numbers");
|
||||
assert_eq!(attributes.execution, SnippetExec::Exec(Default::default()));
|
||||
assert_eq!(attributes.execution, SnippetExecution::Exec(Default::default()));
|
||||
assert!(attributes.line_numbers);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acquire_terminal() {
|
||||
let attributes = parse_attributes("bash +acquire_terminal +exec");
|
||||
assert_eq!(attributes.execution, SnippetExec::AcquireTerminal(Default::default()));
|
||||
assert_eq!(attributes.representation, SnippetRepr::Snippet);
|
||||
assert_eq!(
|
||||
attributes.execution,
|
||||
SnippetExecution::Exec(SnippetExecArgs { repr: SnippetRepr::AcquireTerminal, ..Default::default() })
|
||||
);
|
||||
assert!(!attributes.line_numbers);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image() {
|
||||
let attributes = parse_attributes("bash +image +exec");
|
||||
assert_eq!(attributes.execution, SnippetExec::Exec(Default::default()));
|
||||
assert_eq!(attributes.representation, SnippetRepr::Image);
|
||||
assert_eq!(
|
||||
attributes.execution,
|
||||
SnippetExecution::Exec(SnippetExecArgs { repr: SnippetRepr::Image, ..Default::default() })
|
||||
);
|
||||
assert!(!attributes.line_numbers);
|
||||
}
|
||||
|
||||
@ -877,7 +965,7 @@ mod test {
|
||||
#[test]
|
||||
fn parse_width() {
|
||||
let attributes = parse_attributes("mermaid +width:50% +render");
|
||||
assert_eq!(attributes.representation, SnippetRepr::Render);
|
||||
assert_eq!(attributes.execution, SnippetExecution::Render);
|
||||
assert_eq!(attributes.width, Some(Percent(50)));
|
||||
}
|
||||
|
||||
@ -936,13 +1024,21 @@ println!("Hello world");
|
||||
#[case::exec_replace_and_more("bash +exec_replace:foo +line_numbers", SnippetExecutorSpec::Alternative("foo".into()))]
|
||||
fn alternative_executor(#[case] input: &str, #[case] spec: SnippetExecutorSpec) {
|
||||
let attributes = parse_attributes(input);
|
||||
assert_eq!(attributes.execution, SnippetExec::Exec(spec));
|
||||
let SnippetExecution::Exec(args) = attributes.execution else { panic!("not an exec snippet") };
|
||||
assert_eq!(args.spec, spec);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acquire_terminal_alternative() {
|
||||
let attributes = parse_attributes("bash +acquire_terminal:foo");
|
||||
assert_eq!(attributes.execution, SnippetExec::AcquireTerminal(SnippetExecutorSpec::Alternative("foo".into())));
|
||||
assert_eq!(
|
||||
attributes.execution,
|
||||
SnippetExecution::Exec(SnippetExecArgs {
|
||||
spec: SnippetExecutorSpec::Alternative("foo".into()),
|
||||
repr: SnippetRepr::AcquireTerminal,
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
|
||||
@ -311,6 +311,30 @@ impl Color {
|
||||
Self::Rgb { r, g, b }
|
||||
}
|
||||
|
||||
pub(crate) fn from_8bit(color: u8) -> Option<Self> {
|
||||
match color {
|
||||
0 | 8 => Self::Black.into(),
|
||||
1 | 9 => Self::Red.into(),
|
||||
2 | 10 => Self::Green.into(),
|
||||
3 | 11 => Self::Yellow.into(),
|
||||
4 | 12 => Self::Blue.into(),
|
||||
5 | 13 => Self::Magenta.into(),
|
||||
6 | 14 => Self::Cyan.into(),
|
||||
7 | 15 => Self::White.into(),
|
||||
16..=231 => {
|
||||
let mapping = [0, 95, 95 + 40, 95 + 80, 95 + 120, 95 + 160];
|
||||
let mut value = color - 16;
|
||||
let b = (value % 6) as usize;
|
||||
value /= 6;
|
||||
let g = (value % 6) as usize;
|
||||
value /= 6;
|
||||
let r = (value % 6) as usize;
|
||||
Some(Self::new(mapping[r], mapping[g], mapping[b]))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_rgb(&self) -> Option<(u8, u8, u8)> {
|
||||
match self {
|
||||
Self::Rgb { r, g, b } => Some((*r, *g, *b)),
|
||||
|
||||
@ -31,7 +31,7 @@ use crate::{
|
||||
},
|
||||
third_party::ThirdPartyRender,
|
||||
ui::{
|
||||
execution::output::SnippetHandle,
|
||||
execution::output::WrappedSnippetHandle,
|
||||
footer::{FooterGenerator, FooterVariables},
|
||||
modals::{IndexBuilder, KeyBindingsModalBuilder},
|
||||
separator::RenderSeparator,
|
||||
@ -171,7 +171,7 @@ pub(crate) struct PresentationBuilder<'a, 'b> {
|
||||
bindings_config: KeyBindingsConfig,
|
||||
slides_without_footer: HashSet<usize>,
|
||||
markdown_parser: &'a MarkdownParser<'b>,
|
||||
executable_snippets: HashMap<String, SnippetHandle>,
|
||||
executable_snippets: HashMap<String, WrappedSnippetHandle>,
|
||||
sources: MarkdownSources,
|
||||
options: PresentationBuilderOptions,
|
||||
}
|
||||
|
||||
@ -3,9 +3,9 @@ use crate::{
|
||||
code::{
|
||||
execute::LanguageSnippetExecutor,
|
||||
snippet::{
|
||||
ExternalFile, Highlight, HighlightContext, HighlightGroup, HighlightMutator, HighlightedLine, Snippet,
|
||||
SnippetExec, SnippetExecutorSpec, SnippetLanguage, SnippetLine, SnippetParser, SnippetRepr,
|
||||
SnippetSplitter,
|
||||
ExternalFile, Highlight, HighlightContext, HighlightGroup, HighlightMutator, HighlightedLine, PtyArgs,
|
||||
Snippet, SnippetExecArgs, SnippetExecution, SnippetExecutorSpec, SnippetLanguage, SnippetLine,
|
||||
SnippetParser, SnippetRepr, SnippetSplitter,
|
||||
},
|
||||
},
|
||||
markdown::elements::SourcePosition,
|
||||
@ -19,7 +19,8 @@ use crate::{
|
||||
ui::execution::{
|
||||
RunAcquireTerminalSnippet, RunImageSnippet, SnippetExecutionDisabledOperation, SnippetOutputOperation,
|
||||
disabled::ExecutionType,
|
||||
output::{ExecIndicator, ExecIndicatorStyle, RunSnippetTrigger, SnippetHandle},
|
||||
output::{ExecIndicator, ExecIndicatorStyle, RunSnippetTrigger, SnippetHandle, WrappedSnippetHandle},
|
||||
pty::{PtySnippetHandle, PtySnippetOutputOperation, RunPtySnippetTrigger},
|
||||
validator::ValidateSnippetOperation,
|
||||
},
|
||||
};
|
||||
@ -37,12 +38,14 @@ impl PresentationBuilder<'_, '_> {
|
||||
snippet.attributes.line_numbers = true;
|
||||
}
|
||||
if self.options.auto_render_languages.contains(&snippet.language) {
|
||||
snippet.attributes.representation = SnippetRepr::Render;
|
||||
snippet.attributes.execution = SnippetExecution::Render;
|
||||
}
|
||||
// Ids can only be used in `+exec` snippets.
|
||||
if snippet.attributes.id.is_some()
|
||||
&& (!matches!(snippet.attributes.execution, SnippetExec::Exec(_))
|
||||
|| !matches!(snippet.attributes.representation, SnippetRepr::Snippet))
|
||||
&& !matches!(
|
||||
snippet.attributes.execution,
|
||||
SnippetExecution::Exec(SnippetExecArgs { repr: SnippetRepr::SnippetOutput, .. })
|
||||
)
|
||||
{
|
||||
return Err(self.invalid_presentation(source_position, InvalidPresentation::SnippetIdNonExec));
|
||||
}
|
||||
@ -51,85 +54,110 @@ impl PresentationBuilder<'_, '_> {
|
||||
// Redraw slide if attributes change
|
||||
self.push_differ(format!("{:?}", snippet.attributes));
|
||||
|
||||
let execution_allowed = self.is_execution_allowed(&snippet);
|
||||
match snippet.attributes.representation {
|
||||
SnippetRepr::Render => return self.push_rendered_code(snippet, source_position),
|
||||
SnippetRepr::Image => {
|
||||
if execution_allowed {
|
||||
return self.push_code_as_image(snippet);
|
||||
}
|
||||
}
|
||||
SnippetRepr::ExecReplace => {
|
||||
if execution_allowed {
|
||||
return self.push_replace_code_execution(snippet.clone());
|
||||
}
|
||||
}
|
||||
SnippetRepr::Snippet => (),
|
||||
};
|
||||
if let Some(spec) = &snippet.attributes.validate {
|
||||
// Take the execution spec if we use the default one, otherwise use the one provided.
|
||||
// This allows using `rust +exec:foo +validate` and using `foo` as the executor.
|
||||
let spec = match (spec, &snippet.attributes.execution) {
|
||||
(SnippetExecutorSpec::Default, SnippetExecution::Exec(args)) => &args.spec,
|
||||
_ => spec,
|
||||
};
|
||||
let executor = self.snippet_executor.language_executor(&snippet.language, spec)?;
|
||||
self.push_validator(&snippet, &executor);
|
||||
}
|
||||
|
||||
let block_length = self.push_code_lines(&snippet);
|
||||
match snippet.attributes.execution.clone() {
|
||||
SnippetExec::None => Ok(()),
|
||||
SnippetExec::Exec(_) | SnippetExec::AutoExec(_) | SnippetExec::AcquireTerminal(_) if !execution_allowed => {
|
||||
let mut exec_type = match snippet.attributes.representation {
|
||||
match &snippet.attributes.execution {
|
||||
SnippetExecution::None => {
|
||||
self.push_code_lines(&snippet);
|
||||
}
|
||||
SnippetExecution::Render => {
|
||||
self.push_rendered_code(snippet, source_position)?;
|
||||
}
|
||||
SnippetExecution::Exec(args) if !self.is_execution_allowed(args) => {
|
||||
self.push_code_lines(&snippet);
|
||||
let mut exec_type = match args.repr {
|
||||
SnippetRepr::Image => ExecutionType::Image,
|
||||
SnippetRepr::ExecReplace => ExecutionType::ExecReplace,
|
||||
SnippetRepr::Render | SnippetRepr::Snippet => ExecutionType::Execute,
|
||||
SnippetRepr::SnippetOutput | SnippetRepr::AcquireTerminal => ExecutionType::Execute,
|
||||
};
|
||||
if matches!(snippet.attributes.execution, SnippetExec::AutoExec(_)) {
|
||||
if args.auto {
|
||||
exec_type = ExecutionType::ExecReplace;
|
||||
}
|
||||
self.push_execution_disabled_operation(exec_type);
|
||||
Ok(())
|
||||
}
|
||||
SnippetExec::Exec(spec) | SnippetExec::AutoExec(spec) => {
|
||||
let policy = if matches!(snippet.attributes.execution, SnippetExec::AutoExec(_)) {
|
||||
RenderAsyncStartPolicy::Automatic
|
||||
} else {
|
||||
RenderAsyncStartPolicy::OnDemand
|
||||
};
|
||||
let executor = self.snippet_executor.language_executor(&snippet.language, &spec)?;
|
||||
let alignment = self.code_style(&snippet).alignment;
|
||||
let handle = SnippetHandle::new(snippet.clone(), executor, policy);
|
||||
self.chunk_operations
|
||||
.push(RenderOperation::RenderAsync(Rc::new(RunSnippetTrigger::new(handle.clone()))));
|
||||
self.push_indicator(handle.clone(), block_length, alignment);
|
||||
match snippet.attributes.id.clone() {
|
||||
Some(id) => {
|
||||
if self.executable_snippets.insert(id.clone(), handle).is_some() {
|
||||
return Err(self
|
||||
.invalid_presentation(source_position, InvalidPresentation::SnippetAlreadyExists(id)));
|
||||
}
|
||||
Ok(())
|
||||
SnippetExecution::Exec(args) => {
|
||||
let executor = self.snippet_executor.language_executor(&snippet.language, &args.spec)?;
|
||||
match args.repr {
|
||||
SnippetRepr::Image => {
|
||||
self.push_code_as_image(snippet, executor)?;
|
||||
}
|
||||
None => {
|
||||
self.push_line_break();
|
||||
self.push_code_execution(block_length, handle, alignment)
|
||||
SnippetRepr::ExecReplace => match &args.pty {
|
||||
Some(args) => {
|
||||
let args = args.clone();
|
||||
self.push_replace_pty_code_execution(snippet, executor, args)?;
|
||||
}
|
||||
None => {
|
||||
self.push_replace_code_execution(snippet, executor)?;
|
||||
}
|
||||
},
|
||||
SnippetRepr::AcquireTerminal => {
|
||||
let block_length = self.push_code_lines(&snippet);
|
||||
self.push_acquire_terminal_execution(snippet, block_length, executor)?;
|
||||
}
|
||||
SnippetRepr::SnippetOutput => {
|
||||
let block_length = self.push_code_lines(&snippet);
|
||||
let policy = if args.auto {
|
||||
RenderAsyncStartPolicy::Automatic
|
||||
} else {
|
||||
RenderAsyncStartPolicy::OnDemand
|
||||
};
|
||||
let handle: WrappedSnippetHandle = match &args.pty {
|
||||
Some(args) => PtySnippetHandle::new(snippet.clone(), executor, policy, args.clone()).into(),
|
||||
None => SnippetHandle::new(snippet.clone(), executor, policy).into(),
|
||||
};
|
||||
self.chunk_operations.push(RenderOperation::RenderAsync(handle.build_trigger().into()));
|
||||
|
||||
let alignment = self.code_style(&snippet).alignment;
|
||||
self.push_indicator(handle.clone(), block_length, alignment);
|
||||
match snippet.attributes.id.clone() {
|
||||
Some(id) => {
|
||||
if self.executable_snippets.insert(id.clone(), handle).is_some() {
|
||||
return Err(self.invalid_presentation(
|
||||
source_position,
|
||||
InvalidPresentation::SnippetAlreadyExists(id),
|
||||
));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.push_line_break();
|
||||
match handle {
|
||||
WrappedSnippetHandle::Normal(handle) => {
|
||||
self.push_code_execution(block_length, handle, alignment)?;
|
||||
}
|
||||
WrappedSnippetHandle::Pty(handle) => self.push_pty_code_execution(handle)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SnippetExec::AcquireTerminal(spec) => self.push_acquire_terminal_execution(snippet, block_length, &spec),
|
||||
SnippetExec::Validate(spec) => {
|
||||
let executor = self.snippet_executor.language_executor(&snippet.language, &spec)?;
|
||||
self.push_validator(&snippet, &executor);
|
||||
Ok(())
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn push_detached_code_execution(&mut self, handle: WrappedSnippetHandle) -> BuildResult {
|
||||
match handle {
|
||||
WrappedSnippetHandle::Normal(handle) => {
|
||||
let alignment = self.code_style(&handle.snippet()).alignment;
|
||||
self.push_code_execution(0, handle, alignment)
|
||||
}
|
||||
WrappedSnippetHandle::Pty(handle) => self.push_pty_code_execution(handle),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn push_detached_code_execution(&mut self, handle: SnippetHandle) -> BuildResult {
|
||||
let alignment = self.code_style(&handle.snippet()).alignment;
|
||||
self.push_code_execution(0, handle, alignment)
|
||||
}
|
||||
|
||||
fn is_execution_allowed(&self, snippet: &Snippet) -> bool {
|
||||
match snippet.attributes.representation {
|
||||
SnippetRepr::Snippet => match snippet.attributes.execution {
|
||||
SnippetExec::AutoExec(_) => self.options.enable_snippet_execution_replace,
|
||||
_ => self.options.enable_snippet_execution,
|
||||
},
|
||||
fn is_execution_allowed(&self, args: &SnippetExecArgs) -> bool {
|
||||
match args.repr {
|
||||
SnippetRepr::SnippetOutput | SnippetRepr::AcquireTerminal => self.options.enable_snippet_execution,
|
||||
SnippetRepr::Image | SnippetRepr::ExecReplace => self.options.enable_snippet_execution_replace,
|
||||
SnippetRepr::Render => true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,11 +177,7 @@ impl PresentationBuilder<'_, '_> {
|
||||
block_length
|
||||
}
|
||||
|
||||
fn push_replace_code_execution(&mut self, snippet: Snippet) -> BuildResult {
|
||||
// TODO: representation and execution should probably be merged
|
||||
let SnippetExec::Exec(spec) = snippet.attributes.execution.clone() else {
|
||||
panic!("not an exec snippet");
|
||||
};
|
||||
fn push_replace_code_execution(&mut self, snippet: Snippet, executor: LanguageSnippetExecutor) -> BuildResult {
|
||||
let alignment = match self.code_style(&snippet).alignment {
|
||||
// If we're replacing the snippet output, we have center alignment and no background, use
|
||||
// center alignment but without any margins and minimum sizes so we truly center the output.
|
||||
@ -162,12 +186,22 @@ impl PresentationBuilder<'_, '_> {
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
let executor = self.snippet_executor.language_executor(&snippet.language, &spec)?;
|
||||
let handle = SnippetHandle::new(snippet, executor, RenderAsyncStartPolicy::Automatic);
|
||||
self.chunk_operations.push(RenderOperation::RenderAsync(Rc::new(RunSnippetTrigger::new(handle.clone()))));
|
||||
self.push_code_execution(0, handle, alignment)
|
||||
}
|
||||
|
||||
fn push_replace_pty_code_execution(
|
||||
&mut self,
|
||||
snippet: Snippet,
|
||||
executor: LanguageSnippetExecutor,
|
||||
args: PtyArgs,
|
||||
) -> BuildResult {
|
||||
let handle = PtySnippetHandle::new(snippet, executor, RenderAsyncStartPolicy::Automatic, args);
|
||||
self.chunk_operations.push(RenderOperation::RenderAsync(Rc::new(RunPtySnippetTrigger::new(handle.clone()))));
|
||||
self.push_pty_code_execution(handle)
|
||||
}
|
||||
|
||||
fn load_external_snippet(
|
||||
&mut self,
|
||||
mut code: Snippet,
|
||||
@ -280,10 +314,7 @@ impl PresentationBuilder<'_, '_> {
|
||||
self.chunk_operations.push(RenderOperation::RenderAsync(Rc::new(operation)));
|
||||
}
|
||||
|
||||
fn push_code_as_image(&mut self, snippet: Snippet) -> BuildResult {
|
||||
let executor = self.snippet_executor.language_executor(&snippet.language, &Default::default())?;
|
||||
self.push_validator(&snippet, &executor);
|
||||
|
||||
fn push_code_as_image(&mut self, snippet: Snippet, executor: LanguageSnippetExecutor) -> BuildResult {
|
||||
let operation =
|
||||
RunImageSnippet::new(snippet, executor, self.image_registry.clone(), self.theme.execution_output.status);
|
||||
let operation = RenderOperation::RenderAsync(Rc::new(operation));
|
||||
@ -295,9 +326,8 @@ impl PresentationBuilder<'_, '_> {
|
||||
&mut self,
|
||||
snippet: Snippet,
|
||||
block_length: u16,
|
||||
spec: &SnippetExecutorSpec,
|
||||
executor: LanguageSnippetExecutor,
|
||||
) -> BuildResult {
|
||||
let executor = self.snippet_executor.language_executor(&snippet.language, spec)?;
|
||||
let block_length = self.theme.code.alignment.adjust_size(block_length);
|
||||
let operation = RunAcquireTerminalSnippet::new(
|
||||
snippet,
|
||||
@ -311,7 +341,7 @@ impl PresentationBuilder<'_, '_> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push_indicator(&mut self, handle: SnippetHandle, block_length: u16, alignment: Alignment) {
|
||||
fn push_indicator<T: Into<WrappedSnippetHandle>>(&mut self, handle: T, block_length: u16, alignment: Alignment) {
|
||||
let style = ExecIndicatorStyle {
|
||||
theme: self.theme.execution_output.status,
|
||||
block_length,
|
||||
@ -323,10 +353,7 @@ impl PresentationBuilder<'_, '_> {
|
||||
}
|
||||
|
||||
fn push_code_execution(&mut self, block_length: u16, handle: SnippetHandle, alignment: Alignment) -> BuildResult {
|
||||
let executor = handle.executor();
|
||||
let snippet = handle.snippet();
|
||||
self.push_validator(&snippet, &executor);
|
||||
|
||||
let default_colors = self.theme.default_style.style.colors;
|
||||
let mut execution_output_style = self.theme.execution_output.clone();
|
||||
if snippet.attributes.no_background {
|
||||
@ -346,6 +373,25 @@ impl PresentationBuilder<'_, '_> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push_pty_code_execution(&mut self, handle: PtySnippetHandle) -> BuildResult {
|
||||
let snippet = handle.snippet();
|
||||
let mut style = self.theme.execution_output.clone();
|
||||
if snippet.attributes.no_background {
|
||||
style.style.colors.background = None;
|
||||
style.padding = Default::default();
|
||||
}
|
||||
let operation = PtySnippetOutputOperation::new(
|
||||
handle,
|
||||
style,
|
||||
// execution_output_style,
|
||||
// block_length,
|
||||
self.slide_font_size(),
|
||||
);
|
||||
let operation = RenderOperation::RenderDynamic(Rc::new(operation));
|
||||
self.chunk_operations.push(operation);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push_differ(&mut self, text: String) {
|
||||
self.chunk_operations.push(RenderOperation::RenderDynamic(Rc::new(Differ(text))));
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ use std::io::{self, ErrorKind};
|
||||
///
|
||||
/// This is the same as [crossterm::terminal::window_size] except with some added functionality,
|
||||
/// like implementing `Clone`.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) struct WindowSize {
|
||||
pub(crate) rows: u16,
|
||||
pub(crate) columns: u16,
|
||||
|
||||
@ -34,6 +34,88 @@ impl AnsiParser {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct AnsiColorParser {
|
||||
starting_style: TextStyle,
|
||||
}
|
||||
|
||||
impl AnsiColorParser {
|
||||
pub(crate) fn new(starting_style: TextStyle) -> Self {
|
||||
Self { starting_style }
|
||||
}
|
||||
|
||||
fn parse_8bit(value: u16) -> Option<Color> {
|
||||
Color::from_8bit(value.try_into().unwrap_or(u8::MAX))
|
||||
}
|
||||
|
||||
fn parse_color(iter: &mut ParamsIter) -> Option<Color> {
|
||||
match iter.next()? {
|
||||
[2] => {
|
||||
let r = iter.next()?.first()?;
|
||||
let g = iter.next()?.first()?;
|
||||
let b = iter.next()?.first()?;
|
||||
Self::try_build_rgb_color(*r, *g, *b)
|
||||
}
|
||||
[5] => {
|
||||
let color = *iter.next()?.first()?;
|
||||
Color::from_8bit(color.try_into().unwrap_or(u8::MAX))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_build_rgb_color(r: u16, g: u16, b: u16) -> Option<Color> {
|
||||
let r = r.try_into().ok()?;
|
||||
let g = g.try_into().ok()?;
|
||||
let b = b.try_into().ok()?;
|
||||
Some(Color::new(r, g, b))
|
||||
}
|
||||
|
||||
pub(crate) fn parse(self, mut codes: ParamsIter) -> TextStyle {
|
||||
let mut style = self.starting_style;
|
||||
loop {
|
||||
let Some(&[next]) = codes.next() else {
|
||||
break;
|
||||
};
|
||||
match next {
|
||||
0 => style = Default::default(),
|
||||
1 => style = style.bold(),
|
||||
3 => style = style.italics(),
|
||||
4 => style = style.underlined(),
|
||||
9 => style = style.strikethrough(),
|
||||
39 => {
|
||||
style.colors.foreground = None;
|
||||
}
|
||||
49 => {
|
||||
style.colors.background = None;
|
||||
}
|
||||
30..=37 => {
|
||||
if let Some(color) = Self::parse_8bit(next - 30) {
|
||||
style = style.fg_color(color);
|
||||
}
|
||||
}
|
||||
40..=47 => {
|
||||
if let Some(color) = Self::parse_8bit(next - 40) {
|
||||
style = style.bg_color(color);
|
||||
}
|
||||
}
|
||||
38 => {
|
||||
if let Some(color) = Self::parse_color(&mut codes) {
|
||||
style = style.fg_color(color);
|
||||
}
|
||||
}
|
||||
48 => {
|
||||
if let Some(color) = Self::parse_color(&mut codes) {
|
||||
style = style.bg_color(color);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
style
|
||||
}
|
||||
}
|
||||
|
||||
struct Handler {
|
||||
line: Line,
|
||||
pending_text: Text,
|
||||
@ -55,101 +137,6 @@ impl Handler {
|
||||
self.line.0.push(mem::take(&mut self.pending_text));
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_standard_color(value: u16) -> Option<Color> {
|
||||
let color = match value {
|
||||
0 | 8 => Color::Black,
|
||||
1 | 9 => Color::Red,
|
||||
2 | 10 => Color::Green,
|
||||
3 | 11 => Color::Yellow,
|
||||
4 | 12 => Color::Blue,
|
||||
5 | 13 => Color::Magenta,
|
||||
6 | 14 => Color::Cyan,
|
||||
7 | 15 => Color::White,
|
||||
_ => return None,
|
||||
};
|
||||
Some(color)
|
||||
}
|
||||
|
||||
fn parse_color(iter: &mut ParamsIter) -> Option<Color> {
|
||||
match iter.next()? {
|
||||
[2] => {
|
||||
let r = iter.next()?.first()?;
|
||||
let g = iter.next()?.first()?;
|
||||
let b = iter.next()?.first()?;
|
||||
Self::try_build_rgb_color(*r, *g, *b)
|
||||
}
|
||||
[5] => {
|
||||
let color = *iter.next()?.first()?;
|
||||
match color {
|
||||
0..=15 => Self::parse_standard_color(color),
|
||||
16..=231 => {
|
||||
let mapping = [0, 95, 95 + 40, 95 + 80, 95 + 120, 95 + 160];
|
||||
let mut value = color - 16;
|
||||
let b = (value % 6) as usize;
|
||||
value /= 6;
|
||||
let g = (value % 6) as usize;
|
||||
value /= 6;
|
||||
let r = (value % 6) as usize;
|
||||
Some(Color::new(mapping[r], mapping[g], mapping[b]))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_build_rgb_color(r: u16, g: u16, b: u16) -> Option<Color> {
|
||||
let r = r.try_into().ok()?;
|
||||
let g = g.try_into().ok()?;
|
||||
let b = b.try_into().ok()?;
|
||||
Some(Color::new(r, g, b))
|
||||
}
|
||||
|
||||
fn update_style(&self, mut codes: ParamsIter) -> TextStyle {
|
||||
let mut style = self.style;
|
||||
loop {
|
||||
let Some(&[next]) = codes.next() else {
|
||||
break;
|
||||
};
|
||||
match next {
|
||||
0 => style = Default::default(),
|
||||
1 => style = style.bold(),
|
||||
3 => style = style.italics(),
|
||||
4 => style = style.underlined(),
|
||||
9 => style = style.strikethrough(),
|
||||
39 => {
|
||||
style.colors.foreground = None;
|
||||
}
|
||||
49 => {
|
||||
style.colors.background = None;
|
||||
}
|
||||
30..=37 => {
|
||||
if let Some(color) = Self::parse_standard_color(next - 30) {
|
||||
style = style.fg_color(color);
|
||||
}
|
||||
}
|
||||
40..=47 => {
|
||||
if let Some(color) = Self::parse_standard_color(next - 40) {
|
||||
style = style.bg_color(color);
|
||||
}
|
||||
}
|
||||
38 => {
|
||||
if let Some(color) = Self::parse_color(&mut codes) {
|
||||
style = style.fg_color(color);
|
||||
}
|
||||
}
|
||||
48 => {
|
||||
if let Some(color) = Self::parse_color(&mut codes) {
|
||||
style = style.bg_color(color);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
style
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform for Handler {
|
||||
@ -160,7 +147,7 @@ impl Perform for Handler {
|
||||
fn csi_dispatch(&mut self, params: &vte::Params, _intermediates: &[u8], _ignore: bool, action: char) {
|
||||
if action == 'm' {
|
||||
self.save_pending_text();
|
||||
self.style = self.update_style(params.iter());
|
||||
self.style = AnsiColorParser::new(self.style).parse(params.iter());
|
||||
self.pending_text.style = self.style;
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ use crate::{
|
||||
},
|
||||
terminal::printer::TerminalCommand,
|
||||
};
|
||||
use core::fmt;
|
||||
use std::{collections::HashMap, io};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@ -197,6 +198,20 @@ impl VirtualTerminal {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for VirtualTerminal {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("VirtualTerminal")
|
||||
.field("row", &self.row)
|
||||
.field("column", &self.column)
|
||||
.field("colors", &self.colors)
|
||||
.field("background_color", &self.background_color)
|
||||
.field("images", &self.images)
|
||||
.field("row_heights", &self.row_heights)
|
||||
.field("image_behavior", &self.image_behavior)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TerminalIo for VirtualTerminal {
|
||||
fn execute(&mut self, command: &TerminalCommand<'_>) -> Result<(), TerminalError> {
|
||||
use TerminalCommand::*;
|
||||
|
||||
@ -2,6 +2,7 @@ pub(crate) mod acquire_terminal;
|
||||
pub(crate) mod disabled;
|
||||
pub(crate) mod image;
|
||||
pub(crate) mod output;
|
||||
pub(crate) mod pty;
|
||||
pub(crate) mod validator;
|
||||
|
||||
pub(crate) use acquire_terminal::RunAcquireTerminalSnippet;
|
||||
|
||||
@ -16,7 +16,10 @@ use crate::{
|
||||
},
|
||||
terminal::ansi::AnsiParser,
|
||||
theme::{Alignment, ExecutionOutputBlockStyle, ExecutionStatusBlockStyle},
|
||||
ui::separator::{RenderSeparator, SeparatorWidth},
|
||||
ui::{
|
||||
execution::pty::{PtySnippetHandle, RunPtySnippetTrigger},
|
||||
separator::{RenderSeparator, SeparatorWidth},
|
||||
},
|
||||
};
|
||||
use std::{
|
||||
io::BufRead,
|
||||
@ -150,7 +153,7 @@ impl Pollable for OperationPollable {
|
||||
// Pull data out of the process' output and drop the handle state.
|
||||
let mut state = handle.state.lock().unwrap();
|
||||
let ExecutionState { output, status } = &mut *state;
|
||||
let status = status.clone();
|
||||
let status = *status;
|
||||
|
||||
let modified = output.len() != self.last_length;
|
||||
let mut lines = Vec::new();
|
||||
@ -203,10 +206,6 @@ impl SnippetHandle {
|
||||
Self(Arc::new(Mutex::new(inner)))
|
||||
}
|
||||
|
||||
pub(crate) fn executor(&self) -> LanguageSnippetExecutor {
|
||||
self.0.lock().unwrap().executor.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn snippet(&self) -> Snippet {
|
||||
self.0.lock().unwrap().snippet.clone()
|
||||
}
|
||||
@ -245,16 +244,50 @@ pub(crate) struct ExecIndicatorStyle {
|
||||
pub(crate) alignment: Alignment,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum WrappedSnippetHandle {
|
||||
Normal(SnippetHandle),
|
||||
Pty(PtySnippetHandle),
|
||||
}
|
||||
|
||||
impl WrappedSnippetHandle {
|
||||
pub(crate) fn process_status(&self) -> Option<ProcessStatus> {
|
||||
match self {
|
||||
Self::Normal(handle) => handle.0.lock().unwrap().process_status,
|
||||
Self::Pty(handle) => handle.process_status(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_trigger(&self) -> Box<dyn RenderAsync> {
|
||||
match self.clone() {
|
||||
Self::Normal(handle) => Box::new(RunSnippetTrigger::new(handle)),
|
||||
Self::Pty(handle) => Box::new(RunPtySnippetTrigger::new(handle)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SnippetHandle> for WrappedSnippetHandle {
|
||||
fn from(handle: SnippetHandle) -> Self {
|
||||
Self::Normal(handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PtySnippetHandle> for WrappedSnippetHandle {
|
||||
fn from(handle: PtySnippetHandle) -> Self {
|
||||
Self::Pty(handle)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ExecIndicator {
|
||||
handle: SnippetHandle,
|
||||
handle: WrappedSnippetHandle,
|
||||
separator_width: SeparatorWidth,
|
||||
theme: ExecutionStatusBlockStyle,
|
||||
font_size: u8,
|
||||
}
|
||||
|
||||
impl ExecIndicator {
|
||||
pub(crate) fn new(handle: SnippetHandle, style: ExecIndicatorStyle) -> Self {
|
||||
pub(crate) fn new<T: Into<WrappedSnippetHandle>>(handle: T, style: ExecIndicatorStyle) -> Self {
|
||||
let ExecIndicatorStyle { theme, block_length, font_size, alignment } = style;
|
||||
let block_length = alignment.adjust_size(block_length);
|
||||
let separator_width = match &alignment {
|
||||
@ -265,14 +298,14 @@ impl ExecIndicator {
|
||||
SeparatorWidth::Fixed(block_length.max(MINIMUM_SEPARATOR_WIDTH * font_size as u16))
|
||||
}
|
||||
};
|
||||
let handle = handle.into();
|
||||
Self { handle, separator_width, theme, font_size }
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRenderOperations for ExecIndicator {
|
||||
fn as_render_operations(&self, _dimensions: &WindowSize) -> Vec<RenderOperation> {
|
||||
let inner = self.handle.0.lock().unwrap();
|
||||
let status = &inner.process_status;
|
||||
let status = self.handle.process_status();
|
||||
let description = match status {
|
||||
Some(ProcessStatus::Running) => Text::new("running", self.theme.running_style),
|
||||
Some(ProcessStatus::Success) => Text::new("finished", self.theme.success_style),
|
||||
@ -296,7 +329,7 @@ mod tests {
|
||||
use crate::{
|
||||
code::{
|
||||
execute::SnippetExecutor,
|
||||
snippet::{SnippetAttributes, SnippetExec, SnippetLanguage},
|
||||
snippet::{SnippetAttributes, SnippetExecution, SnippetLanguage},
|
||||
},
|
||||
markdown::{
|
||||
elements::{Line, Text},
|
||||
@ -308,7 +341,10 @@ mod tests {
|
||||
let snippet = Snippet {
|
||||
contents: code.into(),
|
||||
language: SnippetLanguage::Bash,
|
||||
attributes: SnippetAttributes { execution: SnippetExec::Exec(Default::default()), ..Default::default() },
|
||||
attributes: SnippetAttributes {
|
||||
execution: SnippetExecution::Exec(Default::default()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
let executor = SnippetExecutor::default().language_executor(&snippet.language, &Default::default()).unwrap();
|
||||
let policy = RenderAsyncStartPolicy::OnDemand;
|
||||
|
||||
339
src/ui/execution/pty.rs
Normal file
339
src/ui/execution/pty.rs
Normal file
@ -0,0 +1,339 @@
|
||||
use crate::{
|
||||
code::{
|
||||
execute::{LanguageSnippetExecutor, ProcessStatus, PtySnippetContext},
|
||||
snippet::{PtyArgs, Snippet},
|
||||
},
|
||||
markdown::{
|
||||
elements::Text,
|
||||
text_style::{Color, TextStyle},
|
||||
},
|
||||
render::{
|
||||
operation::{
|
||||
AsRenderOperations, BlockLine, Pollable, PollableState, RenderAsync, RenderAsyncStartPolicy,
|
||||
RenderOperation,
|
||||
},
|
||||
properties::WindowSize,
|
||||
},
|
||||
theme::{Alignment, ExecutionOutputBlockStyle},
|
||||
};
|
||||
use portable_pty::{MasterPty, PtySize, native_pty_system};
|
||||
use std::{
|
||||
fmt, io, mem,
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
};
|
||||
|
||||
const BOTTOM_MARGIN_RATIO: f64 = 0.2;
|
||||
const BOTTOM_MINIMUM_MARGIN: u16 = 7;
|
||||
const DEFAULT_COLUMNS: u16 = 80;
|
||||
const DEFAULT_ROWS: u16 = 24;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
enum State {
|
||||
#[default]
|
||||
Initial,
|
||||
Running {
|
||||
pty: PtyMaster,
|
||||
dirty: bool,
|
||||
},
|
||||
ProcessTerminated(ProcessStatus),
|
||||
Done(ProcessStatus),
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
snippet: Snippet,
|
||||
executor: LanguageSnippetExecutor,
|
||||
parser: vt100::Parser,
|
||||
size: WindowSize,
|
||||
update_size: bool,
|
||||
policy: RenderAsyncStartPolicy,
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Inner {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Inner")
|
||||
.field("snippet", &self.snippet)
|
||||
.field("executor", &self.executor)
|
||||
.field("size", &self.size)
|
||||
.field("update_size", &self.update_size)
|
||||
.field("parser", &"...")
|
||||
.field("policy", &self.policy)
|
||||
.field("state", &"...")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PtySnippetOutputOperation {
|
||||
handle: PtySnippetHandle,
|
||||
style: ExecutionOutputBlockStyle,
|
||||
font_size: u8,
|
||||
}
|
||||
|
||||
impl PtySnippetOutputOperation {
|
||||
pub(crate) fn new(handle: PtySnippetHandle, style: ExecutionOutputBlockStyle, font_size: u8) -> Self {
|
||||
Self { handle, style, font_size }
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRenderOperations for PtySnippetOutputOperation {
|
||||
fn as_render_operations(&self, dimensions: &WindowSize) -> Vec<RenderOperation> {
|
||||
let mut inner = self.handle.0.lock().unwrap();
|
||||
let vertical_padding = ((dimensions.rows as f64 * BOTTOM_MARGIN_RATIO) as u16).max(BOTTOM_MINIMUM_MARGIN);
|
||||
let dimensions = dimensions
|
||||
.shrink_rows(vertical_padding / self.font_size as u16)
|
||||
.shrink_columns(dimensions.columns - dimensions.columns / self.font_size as u16);
|
||||
|
||||
if inner.update_size && inner.size != dimensions {
|
||||
inner.size = dimensions;
|
||||
inner.parser.screen_mut().set_size(dimensions.rows, dimensions.columns);
|
||||
}
|
||||
if matches!(inner.state, State::Initial) {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let screen = inner.parser.screen();
|
||||
let (rows, columns) = screen.size();
|
||||
let mut operations = vec![];
|
||||
|
||||
for row in 0..rows {
|
||||
let mut line = Vec::new();
|
||||
let mut current_text = String::new();
|
||||
let mut current_style = TextStyle::default();
|
||||
for column in 0..columns {
|
||||
let cell = screen.cell(row, column).expect("no cell");
|
||||
let mut style = TextStyle::from(cell).size(self.font_size);
|
||||
if style.colors.foreground.is_none() {
|
||||
style.colors.foreground = self.style.style.colors.foreground;
|
||||
}
|
||||
if style.colors.background.is_none() {
|
||||
style.colors.background = self.style.style.colors.background;
|
||||
}
|
||||
let contents = cell.contents();
|
||||
if current_style != style && !current_text.is_empty() {
|
||||
line.push(Text::new(mem::take(&mut current_text), current_style));
|
||||
}
|
||||
current_style = style;
|
||||
if contents.is_empty() {
|
||||
current_text.push(' ');
|
||||
} else {
|
||||
current_text.push_str(contents);
|
||||
}
|
||||
}
|
||||
if !current_text.is_empty() {
|
||||
line.push(Text::new(current_text, current_style));
|
||||
}
|
||||
operations.extend([
|
||||
RenderOperation::RenderBlockLine(BlockLine {
|
||||
prefix: "".into(),
|
||||
right_padding_length: 0,
|
||||
repeat_prefix_on_wrap: false,
|
||||
text: line.into(),
|
||||
block_length: columns,
|
||||
block_color: None,
|
||||
alignment: Alignment::Center {
|
||||
minimum_margin: Default::default(),
|
||||
minimum_size: Default::default(),
|
||||
},
|
||||
}),
|
||||
RenderOperation::RenderLineBreak,
|
||||
]);
|
||||
}
|
||||
operations
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderAsync for PtySnippetOutputOperation {
|
||||
fn pollable(&self) -> Box<dyn Pollable> {
|
||||
Box::new(OperationPollable { handle: self.handle.clone() })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct OperationPollable {
|
||||
handle: PtySnippetHandle,
|
||||
}
|
||||
|
||||
impl OperationPollable {
|
||||
fn spawn(ctx: PtySnippetContext, dimensions: WindowSize, handle: PtySnippetHandle) -> anyhow::Result<PtyMaster> {
|
||||
let pty_system = native_pty_system();
|
||||
let pty_size = PtySize {
|
||||
rows: dimensions.rows,
|
||||
cols: dimensions.columns,
|
||||
pixel_width: dimensions.pixels_per_column() as u16,
|
||||
pixel_height: dimensions.pixels_per_row() as u16,
|
||||
};
|
||||
let pair = pty_system.openpty(pty_size)?;
|
||||
pair.slave.spawn_command(ctx.command.clone())?;
|
||||
PtyMaster::new(pair.master, handle, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Pollable for OperationPollable {
|
||||
fn poll(&mut self) -> PollableState {
|
||||
let mut inner = self.handle.0.lock().unwrap();
|
||||
let current_size = inner.size;
|
||||
match &mut inner.state {
|
||||
State::Initial => match inner.executor.pty_execution_context(&inner.snippet) {
|
||||
Ok(ctx) => match Self::spawn(ctx, inner.size, self.handle.clone()) {
|
||||
Ok(pty) => {
|
||||
inner.state = State::Running { pty, dirty: true };
|
||||
PollableState::Modified
|
||||
}
|
||||
Err(e) => {
|
||||
inner.state = State::Done(ProcessStatus::Failure);
|
||||
PollableState::Failed { error: format!("failed to run script: {e}") }
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
inner.state = State::Done(ProcessStatus::Failure);
|
||||
PollableState::Failed { error: format!("failed to run script: {e}") }
|
||||
}
|
||||
},
|
||||
State::Running { dirty, pty } => {
|
||||
if let Ok(size) = pty._master.get_size() {
|
||||
if size.rows != current_size.rows || size.cols != current_size.columns {
|
||||
let size = PtySize {
|
||||
rows: current_size.rows,
|
||||
cols: current_size.columns,
|
||||
pixel_width: 0,
|
||||
pixel_height: 0,
|
||||
};
|
||||
let _ = pty._master.resize(size);
|
||||
}
|
||||
}
|
||||
if mem::take(dirty) { PollableState::Modified } else { PollableState::Unmodified }
|
||||
}
|
||||
State::ProcessTerminated(status) => {
|
||||
inner.state = State::Done(*status);
|
||||
PollableState::Modified
|
||||
}
|
||||
_ => PollableState::Unmodified,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PtyMaster {
|
||||
_master: Box<dyn MasterPty>,
|
||||
_ctx: PtySnippetContext,
|
||||
}
|
||||
|
||||
impl PtyMaster {
|
||||
fn new(master: Box<dyn MasterPty>, handle: PtySnippetHandle, ctx: PtySnippetContext) -> anyhow::Result<Self> {
|
||||
let reader = master.try_clone_reader()?;
|
||||
thread::spawn(|| process_output(reader, handle));
|
||||
Ok(Self { _master: master, _ctx: ctx })
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for PtyMaster {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("PtyMaster").field("master", &"...").finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn process_output(mut reader: Box<dyn io::Read>, handle: PtySnippetHandle) {
|
||||
let mut input_buffer = [0; 1024];
|
||||
let status = loop {
|
||||
let Ok(bytes_read) = reader.read(&mut input_buffer) else {
|
||||
break ProcessStatus::Failure;
|
||||
};
|
||||
if bytes_read == 0 {
|
||||
break ProcessStatus::Success;
|
||||
}
|
||||
let bytes = &input_buffer[..bytes_read];
|
||||
let mut inner = handle.0.lock().unwrap();
|
||||
inner.parser.process(bytes);
|
||||
if let State::Running { dirty, .. } = &mut inner.state {
|
||||
*dirty = true;
|
||||
};
|
||||
};
|
||||
handle.0.lock().unwrap().state = State::ProcessTerminated(status);
|
||||
}
|
||||
|
||||
impl From<&vt100::Cell> for TextStyle {
|
||||
fn from(cell: &vt100::Cell) -> Self {
|
||||
let mut style = TextStyle::default();
|
||||
if cell.bold() {
|
||||
style = style.bold();
|
||||
}
|
||||
if cell.italic() {
|
||||
style = style.italics();
|
||||
}
|
||||
if cell.underline() {
|
||||
style = style.underlined();
|
||||
}
|
||||
style.colors.foreground = parse_color(cell.fgcolor());
|
||||
style.colors.background = parse_color(cell.bgcolor());
|
||||
style
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_color(color: vt100::Color) -> Option<Color> {
|
||||
match color {
|
||||
vt100::Color::Default => None,
|
||||
vt100::Color::Idx(value) => Color::from_8bit(value),
|
||||
vt100::Color::Rgb(r, g, b) => Some(Color::Rgb { r, g, b }),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PtySnippetHandle(Arc<Mutex<Inner>>);
|
||||
|
||||
impl PtySnippetHandle {
|
||||
pub(crate) fn new(
|
||||
snippet: Snippet,
|
||||
executor: LanguageSnippetExecutor,
|
||||
policy: RenderAsyncStartPolicy,
|
||||
args: PtyArgs,
|
||||
) -> Self {
|
||||
let size = WindowSize {
|
||||
columns: args.columns.unwrap_or(DEFAULT_COLUMNS),
|
||||
rows: args.rows.unwrap_or(DEFAULT_ROWS),
|
||||
height: 0,
|
||||
width: 0,
|
||||
};
|
||||
let update_size = args.columns.is_none() || args.rows.is_none();
|
||||
let parser = vt100::Parser::new(size.rows, size.columns, 1000);
|
||||
let inner = Inner { snippet, executor, parser, size, update_size, state: Default::default(), policy };
|
||||
Self(Arc::new(Mutex::new(inner)))
|
||||
}
|
||||
|
||||
pub(crate) fn snippet(&self) -> Snippet {
|
||||
self.0.lock().unwrap().snippet.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn process_status(&self) -> Option<ProcessStatus> {
|
||||
match &self.0.lock().unwrap().state {
|
||||
State::Initial => None,
|
||||
State::Running { .. } => Some(ProcessStatus::Running),
|
||||
State::ProcessTerminated(status) | State::Done(status) => Some(*status),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RunPtySnippetTrigger(PtySnippetHandle);
|
||||
|
||||
impl RunPtySnippetTrigger {
|
||||
pub(crate) fn new(handle: PtySnippetHandle) -> Self {
|
||||
Self(handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRenderOperations for RunPtySnippetTrigger {
|
||||
fn as_render_operations(&self, _dimensions: &WindowSize) -> Vec<RenderOperation> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderAsync for RunPtySnippetTrigger {
|
||||
fn pollable(&self) -> Box<dyn Pollable> {
|
||||
Box::new(OperationPollable { handle: self.0.clone() })
|
||||
}
|
||||
|
||||
fn start_policy(&self) -> RenderAsyncStartPolicy {
|
||||
self.0.0.lock().unwrap().policy
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user