[Feature] Magic Inputs
Idea
This feature would allow users to type text input that would be inlined in code with magic command, and text input change would not have any affect on markdown note.
Example
```run-rust {inputs=["Insert Value:"]}
fn main()
{
let value = @input(0);
println!("{value}");
}
Preview

Inlined code for example
fn main()
{
let value = "test";
println!("{value}");
}
Insert html template:
<label for="code-input-{id}">{label}</label>
<input type="{specified type}" id="code-input-{id}" value="{default value}" pattern="{validation}">
Advanced options
In simple example above we would just declare input label and would assume input is text. In more advanced case I think input syntax should allow specifying label, default value and input type (text, checkbox, text with input validation with regex).
Interesting! Thank you for this idea.
We currently allow input on stdin (after a program doesn't output for a time, you should see an input box appear), but this seems like a lower-friction option for input to a program.
Would it require all inputs to be entered before starting the program?
Would it require all inputs to be entered before starting the program?
Well I think unpopulated inputs should be considered as empty strings as long as validation allows empty strings. If validation fails execution should be blocked.
Also I don't have much experience with JS/TS but I think I could do initial PR with support for inputs, but I want to hear more opinions first.
I think this idea is interesting for border cases where you want to write code that needs input but don't want to type in the input every time. I'm sure this is helpful in some cases and doesn't do any harm. But it would be better to throw an exception if no input is given to prevent unexpected behavior.
Another similar idea: Sometimes one writes a program, that reads in a file. It could be helpful to automatically create a dummy file with content defined somewhere in the obsidian note.
But it would be better to throw an exception if no input is given to prevent unexpected behavior.
Yes but than you cannot have empty string as input which shouldn't be the case.
Another similar idea: Sometimes one writes a program, that reads in a file. It could be helpful to automatically create a dummy file with content defined somewhere in the obsidian note.
This is good for stdin feature since you can just pipe it there.
I think it would be best to add few predefined validations like
default => * //any input allowed
text => + //must have at least one character
number => \d+ //only letters
decimal => \d+(.\d+){0,1} // number with decimal point
and whatever else like email or what people demand
Also to mention you should be able to always write regex yourself instead of using predefined validation
This is good for stdin feature since you can just pipe it there.
I think that the utility would come from being able to type a code block, which is then automagically put into a file for the runtime environment. The stdin feature allows users to type in input, but this is difficult to save for reproducibility
For those who want this feature right now, can use Execute code with Obsidian meta bind. That plugin allows to place inputs which save their values into metadata. Then you only need to parse current file like this:
def r(vpath, npath):
"""read metadata of note (npath - path to note) in vault (vpath - path to vault)"""
fpath = f'{vpath}/{npath}'
d = {}
with open(fpath, encoding='utf-8') as f:
for l in f:
if l == '---\n': break
for l in f:
if l == '---\n': break
k, v = l[:-1].split(': ')
d[k] = v[1:-1]
return d
obsidian_params = r(@vault_path, @note_path)
Actually it would be nice if something like @note_meta, @get_meta(key) and @set_meta(key, value) existed.
Added possibility to parse complex yaml and writing it back. But please backup your file before using, I don't test it much.
import ruamel.yaml
from io import StringIO
yaml = ruamel.yaml.YAML()
def r(vpath, npath):
"""read metadata of note (npath - path to note) in vault (vpath - path to vault)"""
fpath = f'{vpath}/{npath}'
with open(fpath, encoding='utf-8') as f:
# Check if file has any metadata
for i, l in enumerate(f):
if l == '---\n' and i == 0:
break
else:
return {}
lst = []
for l in f:
if l == '---\n':
break
lst.append(l)
# k, v = l[:-1].split(': ')
# d[k] = v[1:-1]
chunk = "".join(lst)
dct = yaml.load(chunk)
return dct if dct is not None else {}
def w(vpath, npath, dct):
orig_dct = r(vpath, npath)
sio = StringIO()
yaml.dump(dct, sio)
new_meta = sio.getvalue()
sio.close()
fpath = f'{vpath}/{npath}'
until_line = line_count(orig_dct)
with open(fpath, 'r', encoding='utf-8') as fi:
with open(fpath, 'r+', encoding='utf-8') as fo:
# not implemented yet
if not is_meta_exists():
add_empty_meta()
# first line is ---
cur_lineno = 1
fi.readline()
seek_pos = fi.tell()
fo.seek(seek_pos, 0)
# skip old meta
while cur_lineno <= until_line:
cur_lineno += 1
fi.readline()
# insert our new meta
fo.writelines(new_meta)
# insert rest of the file
cur_line = fi.readline()
while cur_line:
fo.writelines(cur_line)
cur_line = fi.readline()
fo.truncate()
def line_count(dct):
count = 0
for v in list(dct.values()):
# check multiline
ml_count = v.count('\n')
ml_count = ml_count + 1 if ml_count > 0 else 0
count += 1 + ml_count
return count
def is_meta_exists():
return True
def add_empty_meta():
pass
I came here looking for a way to get Obsidian meta bind. I think it would be so cool if there was a way to access frontmatter properties so that you could easily get the value of them in your code blocks. If anybody knows of a way to do this using any other community plugins I'm all ears!
I am going to leave this here in case anybody is looking for a way to do this using PowerShell. I keep my PowerShell functions in my vault so you can see I dot source the function Get-NoteFrontmatter.ps1 in. The following code goes into the "Inject Powershell code" field found in the Language-specific settings area of Execute Code.
# Returns YAML frontmatter in current note
Import-Module powershell-yaml
. (Join-Path -Path @vault_path -ChildPath 'PowerShell' -AdditionalChildPath 'Functions', 'Get-NoteFrontmatter.ps1')
$meta = Get-NoteFrontmatter -Path (Join-Path @vault_path @note_path)
$meta = ConvertFrom-Yaml $meta
The Get-NoteFrontmatter.ps1 function is below:
function Get-NoteFrontmatter {
param(
[string]$Path
)
$reader = New-Object System.IO.StreamReader($Path)
$yamlContent = @()
$capture = $false
try {
while ($reader.Peek() -ge 0) {
$line = $reader.ReadLine()
if ($line -eq '---') {
if ($capture) {
break
} else {
$capture = $true
}
} elseif ($capture) {
$yamlContent += $line
}
}
} finally {
$reader.Close()
}
return $yamlContent -join "`n"
}
It only reads in the content between --- and --- and then uses the powershell-yaml module to parse it into valid YAML. From there, you can access the metadata values in your frontmatter by using the $meta variable in any of your code blocks. For example, if I use the Meta Bind plugin to create an "inline select" field like so:
INPUT[inlineSelect(option(2020), option(2021), option(2022),option(2023)):year]
I can now reference that value in my PowerShell code blocks like this:
Write-Host "Selected year is: $($meta.year)"
Hope this helps someone.
I only needed a quick and dirty way using Deno/Typescript; this is how I'm getting a value:
const getPropertyRegex = (text, key) => (text.match(new RegExp(`aria-label="${key}">.*?metadata-property-value.*?></div><div.*?>(?<value>.*?)</div`)) ?? { groups: { value: "" }}).groups.value;
const value = getPropertyRegex(@content, "YT_API_KEY")
console.log(value)
And in my Obsidian note, I use meta-bind to set those values:
`INPUT[text(placeholder(YT_API_KEY)):YT_API_KEY]`
Meta bind edits the frontmatter property; then I can read it in my typescript code.
If you want something more sturdy, you can use this:
import {parse as yamlParse} from "jsr:@std/[email protected]";
const _noteData = (() => {
const filePath = @vault_path + "/" + @note_path
const contentRaw = Deno.readTextFileSync(filePath);
const [, frontmatterString, content] = contentRaw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)/) ?? [];
return yamlParse(frontmatterString);
})()
Now you can get all the data in _note_data in any TS block:
console.log(_noteData)
If you want to stick that in the "Inject Typescript code" block of the plugin settings, it's a bit more complicated as it seems that part remains cached, so using import and const doesn't work. But you can work around that:
;await (async () => {
const __dirname = [@vault_path, ... @note_path.split("/").slice(0, -1)].join("/") + "/";
Deno.noColor = true
Deno.chdir(__dirname);
const loadLocal = async (name: string) => await import(__dirname + name + '.ts');
const parseNoteFrontMatter = async (filePath: string) => {
const {parse: yamlParse} = await import("jsr:@std/[email protected]");
const contentRaw = Deno.readTextFileSync(filePath);
const [, frontmatterString, _content] = contentRaw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)/) ?? [];
return yamlParse(frontmatterString);
}
const _noteData = await parseNoteFrontMatter(@vault_path + "/" + @note_path);
globalThis.loadLocal = loadLocal;
globalThis._noteData = _noteData;
globalThis.parseNoteFrontMatter = parseNoteFrontMatter;
})();
I also needed this in shell blocks, so this is my injection script:
file_path=@vault_path/@note_path
cd "$(dirname "$file_path")"
get_frontmatter() {
local key="$1"
local file_path=@vault_path/@note_path
sed -n '1{/^---$/!q;};2,/^---$/{/^---$/q; p;}' "$file_path" | \
grep "^${key}:" | \
sed "s/^${key}:[[:space:]]*//" | \
sed 's/^["'\'']\(.*\)["'\'']$/\1/'
}
Then in my shell scripts I can do:
echo "Description: $(get_frontmatter "description")"
echo "Date: $(get_frontmatter "date_modified")"
echo "Custom Property: $(get_frontmatter "custom_prop")"
This isn't very sturdy (you won't get arrays and such), but it is very portable and doesn't require utilities that may not exist on different machines.