Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,13 @@ upmd --ci --all README.md
```

> [!TIP]
> With no path, `upmd` opens `up.md` or `UP.md` from the current directory when either is present. Otherwise, it opens the current-directory file picker. Directories are searched recursively, while piped stdin is read as Markdown. A failed task or startup error produces a non-zero exit status.
> No path: opens `up.md`/`UP.md` or the file picker. Directory searches are recursive. Piped stdin is Markdown.

Run `upmd --help` for all command-line options.

> [!TIP]
> Tasks start in the current directory by default. Use `-d DIR` to choose an initial directory, or `-d :auto` to use the Markdown file's directory. For stdin, `:auto` uses the current directory.

## Tasks and workflows

The workflow in [`DEMO.md`](DEMO.md) uses `name` and `deps` attributes after the language token:
Expand Down
7 changes: 5 additions & 2 deletions src/apps/cli/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,7 @@ impl App {
// see mutations from prior blocks (like cd or export).
let envs = std::env::vars().collect();
let cwd = config
.working_dir
.clone()
.resolve_working_dir()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());

Self {
Expand Down Expand Up @@ -143,6 +142,10 @@ impl App {
return Some(Cmd::quit());
}
self.picker = None;
self.config.file = Some(path.display().to_string());
if let Some(cwd) = self.config.resolve_working_dir() {
self.cwd = cwd;
}
}
Err(err) => {
// In picker mode, errors are fatal since there's no
Expand Down
27 changes: 24 additions & 3 deletions src/apps/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::apps::theme::Theme;
use keymap::{DerivedConfig, KeyMapConfig};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::path::PathBuf;
use std::path::{Path, PathBuf};

pub const LOGO: [&str; 4] = [
" ▄",
Expand Down Expand Up @@ -581,6 +581,13 @@ impl UserConfig {
}
}

/// Working-directory selection for code execution.
#[derive(Debug, Clone)]
pub enum WorkingDir {
Auto,
Path(PathBuf),
}

/// Merged application configuration from CLI arguments, user config, and defaults.
#[derive(Debug, Default, Clone)]
pub struct Config {
Expand All @@ -600,7 +607,7 @@ pub struct Config {
/// Per-language binary overrides.
pub binaries: HashMap<String, upmd_runner::RunnerOptions>,
/// Working directory for code execution.
pub working_dir: Option<PathBuf>,
pub working_dir: Option<WorkingDir>,
}

pub type Envs = BTreeMap<String, String>;
Expand All @@ -623,10 +630,24 @@ pub struct ConfigArgs {
pub transparent: bool,
pub keymap: KeymapConfig,
pub binaries: HashMap<String, upmd_runner::RunnerOptions>,
pub working_dir: Option<PathBuf>,
pub working_dir: Option<WorkingDir>,
}

impl Config {
/// Resolves the configured directory against the active Markdown file.
pub fn resolve_working_dir(&self) -> Option<PathBuf> {
match self.working_dir.as_ref()? {
WorkingDir::Auto => self.file.as_deref().map(|file| {
Path::new(file)
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."))
.to_path_buf()
}),
WorkingDir::Path(path) => Some(path.clone()),
}
}

pub fn new(args: ConfigArgs) -> Self {
Self {
theme: Theme::new(&args.theme, args.transparent),
Expand Down
18 changes: 10 additions & 8 deletions src/apps/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub struct App {
file_picker_root: Option<PathBuf>,
output: tui::output::Output,
workflow: WorkflowState,
last_cwd: Option<PathBuf>,
cwd: Option<PathBuf>,
started: bool,
/// Transient flash notification at the bottom right.
notification: Option<tui::notification::FlashMessage>,
Expand Down Expand Up @@ -160,7 +160,9 @@ impl App {
file_picker_root: None,
output: tui::output::Output::new(config.keymap.output()),
workflow: WorkflowState::default(),
last_cwd: None,
cwd: config
.resolve_working_dir()
.or_else(|| std::env::current_dir().ok()),
notification: None,
started: false,
}
Expand All @@ -181,10 +183,7 @@ impl App {
envs,
self.config.capture_state,
&self.config.binaries,
self.config
.working_dir
.clone()
.or_else(|| self.last_cwd.clone()),
self.cwd.clone(),
)
.map(|receiver| exec::stream_rx(id, receiver, Msg::StreamUpdate));

Expand Down Expand Up @@ -333,7 +332,7 @@ impl App {
self.envs.merge_envs(env);
}
if let Some(cwd) = cwd {
self.last_cwd = Some(cwd);
self.cwd = Some(cwd);
}
return;
}
Expand All @@ -347,7 +346,7 @@ impl App {
self.envs.merge_envs(envs);
}
if let Some(cwd) = state.cwd {
self.last_cwd = Some(cwd);
self.cwd = Some(cwd);
}
}
}
Expand Down Expand Up @@ -1342,6 +1341,9 @@ impl App {
match self.load_document(doc) {
Ok(()) => {
self.config.file = Some(path.display().to_string());
if let Some(cwd) = self.config.resolve_working_dir() {
self.cwd = Some(cwd);
}
self.overlay = None;
self.view = View::Home;
}
Expand Down
11 changes: 6 additions & 5 deletions src/args.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use std::path::PathBuf;

use clap::{ArgAction, Parser};

use crate::apps::config::{Config, ConfigArgs, UserConfig};
use crate::apps::config::{Config, ConfigArgs, UserConfig, WorkingDir};

/// Run code blocks from Markdown in the terminal.
#[derive(Parser, Debug)]
Expand All @@ -23,7 +21,7 @@ pub struct Args {
#[arg(long, default_value_t = false)]
pub ci: bool,

/// Working directory for code execution (default: current directory).
/// Initial working directory for code execution (:auto uses the Markdown file's directory).
#[arg(short = 'd', long, value_name = "DIR")]
pub working_dir: Option<String>,

Expand Down Expand Up @@ -91,7 +89,10 @@ impl From<Args> for Config {
/// CLI flags explicitly set by the user take precedence over the config file.
pub fn build_config(args: Args, user_cfg: UserConfig) -> Config {
let theme = args.theme.unwrap_or_else(|| user_cfg.theme().to_string());
let working_dir = args.working_dir.map(PathBuf::from);
let working_dir = args.working_dir.map(|dir| match dir.as_str() {
":auto" => WorkingDir::Auto,
_ => WorkingDir::Path(dir.into()),
});
let transparent = args
.tui_args
.transparent
Expand Down
24 changes: 24 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ printf 'FINAL-PTY-TAIL\n'
);
}

#[cfg(unix)]
#[test]
fn auto_working_directory_uses_markdown_parent() {
let tmp = tempfile::tempdir().unwrap();
let docs = tmp.path().join("docs");
std::fs::create_dir(&docs).unwrap();
std::fs::write(docs.join("marker.txt"), "markdown-directory\n").unwrap();
std::fs::write(docs.join("run.md"), "```sh\ncat marker.txt\n```\n").unwrap();

let output = Command::new(env!("CARGO_BIN_EXE_upmd"))
.current_dir(tmp.path())
.args(["docs/run.md", "--ci", "-b", "1", "-d", ":auto"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);

assert!(output.status.success(), "{stdout}\n{stderr}");
assert!(
stdout.lines().any(|line| line == "markdown-directory"),
"{stdout}"
);
}

#[cfg(unix)]
#[test]
fn missing_block_fails_before_either_frontend_starts() {
Expand Down
Loading