obsidian-execute-code icon indicating copy to clipboard operation
obsidian-execute-code copied to clipboard

[Feature] Magic Inputs

Open JPaja opened this issue 3 years ago • 12 comments

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

image

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).

JPaja avatar Jan 08 '23 15:01 JPaja

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?

chlohal avatar Jan 20 '23 17:01 chlohal

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.

JPaja avatar Jan 20 '23 18:01 JPaja

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.

twibiral avatar Jan 25 '23 11:01 twibiral

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.

JPaja avatar Jan 25 '23 12:01 JPaja

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

JPaja avatar Jan 25 '23 12:01 JPaja

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

chlohal avatar Jan 25 '23 17:01 chlohal

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.

shin0kaze avatar Apr 27 '23 15:04 shin0kaze

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

shin0kaze avatar Jun 09 '23 11:06 shin0kaze

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!

fatherofinvention avatar Jun 14 '23 22:06 fatherofinvention

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)"
ed5kmNvg20230614

Hope this helps someone.

fatherofinvention avatar Jun 14 '23 23:06 fatherofinvention

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;

})();

Xananax avatar Sep 27 '25 16:09 Xananax

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.

Xananax avatar Oct 09 '25 14:10 Xananax