Skip to content
Open
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
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ tui-input = "0.11.0"
syntect-tui = "3.0.4"
syntect = "5.2.0"
graphql-parser = "0.4.0"
minijinja = { version = "2", features = ["json"] }

[dev-dependencies]
mktemp = "0.5.1"
2 changes: 1 addition & 1 deletion example/create_issue.http
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ Accept: application/vnd.github+json
{
"title": "Found a bug",
"body": "I'm having a problem with this.",
"labels": [ {{ label[, ]["] }} ],
"labels": ["{{ label | select_multiple | join('", "') }}"],
Copy link
Copy Markdown
Contributor Author

@seblyng seblyng Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now have something very powerful like jinja (if we merge this), and we can potentially avoid some hardcoded restrictions we have. For example, we have a requirement now for a list to be like this:

[[label]]
name = "foo"
value = "foo"

[[label]]
name = "bar"
value = "bar"

So name and value keys.

I propose that we at least keep it like this for this PR to make the scope a bit smaller, and then potentially do another breaking change if we want.

We can avoid to "map" it internally (like we do in for example the list_option_to_string and other places) and instead just do it like this in the template:

["{{ label | select_multiple | map(attribute="value") | join('", "') }}"]

I haven't thought about potential pitfalls with this though

}
2 changes: 1 addition & 1 deletion example/get_issues.http
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
GET {{base_url}}/repos/{{reponame}}/issues?page={{page|1}}&per_page=10 HTTP/1.1
GET {{base_url}}/repos/{{reponame}}/issues?page={{page | fallback(1)}}&per_page=10 HTTP/1.1
Accept: application/vnd.github+json
2 changes: 1 addition & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub struct Args {
conflicts_with = "name",
conflicts_with = "repeat",
conflicts_with = "flurry",
conflicts_with = "watch",
conflicts_with = "watch"
)]
pub select: Option<Option<String>>,

Expand Down
6 changes: 4 additions & 2 deletions src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ impl CookieStore for HitmanCookieJar {
}

fn cookies(&self, _: &Url) -> Option<reqwest::header::HeaderValue> {
let data_file = read_toml(&self.root_dir.join(DATA_FILE)).ok().flatten()?;
let data_file =
read_toml(&self.root_dir.join(DATA_FILE)).ok().flatten()?;

match data_file.get(COOKIE_KEY)? {
Value::Array(arr) => {
Expand Down Expand Up @@ -213,7 +214,8 @@ fn merge(config: &mut TomlTable, other: TomlTable) {
fn read_toml(file_path: &Path) -> Result<Option<TomlTable>> {
match fs::read_to_string(file_path) {
Ok(content) => {
let cfg = toml::from_str::<TomlTable>(&content).with_context(|| format!("When reading {file_path:?}"))?;
let cfg = toml::from_str::<TomlTable>(&content)
.with_context(|| format!("When reading {file_path:?}"))?;

Ok(Some(cfg))
}
Expand Down
1 change: 0 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ pub mod substitute;
pub mod util;

pub mod prompt;

27 changes: 15 additions & 12 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use std::env::current_dir;
use tokio::sync::mpsc;

use hitman::env::{
find_available_requests, get_target, load_env,
select_target, set_target, watch_list,
find_available_requests, get_target, load_env, select_target, set_target,
watch_list,
};
use hitman::flurry::flurry_attack;
use hitman::monitor::monitor;
Expand All @@ -34,7 +34,8 @@ async fn main() -> Result<()> {
set_interactive_mode(!(args.non_interactive || args.watch));

if let Some(arg) = args.select {
let root_dir = find_root_dir(&current_dir()?)?.context("No hitman.toml found")?;
let root_dir =
find_root_dir(&current_dir()?)?.context("No hitman.toml found")?;

match arg {
Some(target) => set_target(&root_dir, &target)?,
Expand All @@ -49,7 +50,10 @@ async fn main() -> Result<()> {
let file_path = cwd.join(file_path);
let resolved = resolve_path(&file_path)?;

let target = args.target.clone().unwrap_or_else(|| get_target(&resolved.root_dir));
let target = args
.target
.clone()
.unwrap_or_else(|| get_target(&resolved.root_dir));

if let Some(flurry_size) = args.flurry {
let scope = load_env(&target, &resolved, &args.options)?;
Expand All @@ -64,8 +68,7 @@ async fn main() -> Result<()> {
let scope = load_env(&target, &resolved, &args.options)?;
monitor(&resolved, delay_seconds, &scope).await
} else {
let res =
run_once(&target, &resolved, &args.options).await;
let res = run_once(&target, &resolved, &args.options).await;

if args.watch {
watch_mode(&target, &resolved, &args.options).await
Expand All @@ -90,10 +93,12 @@ async fn main() -> Result<()> {

let file_path = &files[selected.index];
let resolved = resolve_path(file_path)?;
let target = args.target.clone().unwrap_or_else(|| get_target(&resolved.root_dir));
let target = args
.target
.clone()
.unwrap_or_else(|| get_target(&resolved.root_dir));

let result =
run_once(&target, &resolved, &args.options).await;
let result = run_once(&target, &resolved, &args.options).await;

if !args.repeat {
break result;
Expand Down Expand Up @@ -154,9 +159,7 @@ async fn watch_mode(
if let Some(event) = rx.recv().await {
if let EventKind::Modify(_) = event.kind {
watcher.unwatch_all()?;
if let Err(err) =
run_once(target, resolved, options).await
{
if let Err(err) = run_once(target, resolved, options).await {
error!("# {err}");
}
watcher.watch_all()?;
Expand Down
55 changes: 26 additions & 29 deletions src/prompt.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anyhow::{bail, Result};
use fuzzy_matcher::skim::SkimMatcherV2;
use inquire::{list_option::ListOption, DateSelect, MultiSelect, Select, Text};
use minijinja::Value as JinjaValue;
use std::{collections::HashMap, env, string::ToString};
use toml::Value;

Expand All @@ -11,7 +12,6 @@ use crate::{
substitute::{
prepare_request,
Substitution::{Complete, ValueMissing},
SubstitutionValue,
},
};

Expand Down Expand Up @@ -47,12 +47,12 @@ pub fn get_interaction() -> Box<dyn UserInteraction> {

pub trait UserInteraction {
fn prompt(&self, key: &str, fallback: Option<&str>) -> Result<String>;
fn select(&self, key: &str, values: &[Value]) -> Result<String>;
fn select(&self, key: &str, values: &[Value]) -> Result<JinjaValue>;
fn select_multiple(
&self,
key: &str,
values: &[Value],
) -> Result<Vec<String>>;
) -> Result<JinjaValue>;
}

pub fn prepare_request_interactive<I>(
Expand All @@ -63,7 +63,7 @@ pub fn prepare_request_interactive<I>(
where
I: UserInteraction + ?Sized,
{
let mut vars = HashMap::new();
let mut vars: HashMap<String, JinjaValue> = HashMap::new();

loop {
match prepare_request(resolved, &vars)? {
Expand All @@ -74,23 +74,15 @@ where
multiple,
} => {
let value = match scope.lookup(&key)? {
Replacement::Value(value) => {
SubstitutionValue::Single(value)
}
Replacement::ValueNotFound { key } => {
SubstitutionValue::Single(
interaction.prompt(&key, fallback.as_deref())?,
)
}
Replacement::Value(value) => JinjaValue::from(value),
Replacement::ValueNotFound { key } => JinjaValue::from(
interaction.prompt(&key, fallback.as_deref())?,
),
Replacement::MultipleValuesFound { key, values } => {
if multiple {
SubstitutionValue::Multiple(
interaction.select_multiple(&key, &values)?,
)
interaction.select_multiple(&key, &values)?
} else {
SubstitutionValue::Single(
interaction.select(&key, &values)?,
)
interaction.select(&key, &values)?
}
}
};
Expand Down Expand Up @@ -122,11 +114,10 @@ impl UserInteraction for NoUserInteraction {
if let Some(val) = fallback.map(ToString::to_string) {
return Ok(val);
}

bail!("Replacement not found: {key}");
}

fn select(&self, key: &str, values: &[toml::Value]) -> Result<String> {
fn select(&self, key: &str, values: &[toml::Value]) -> Result<JinjaValue> {
let suggestions = self.get_suggestions(key, values);
bail!("Replacement not selected: {key}\nSuggestions:\n{suggestions}");
}
Expand All @@ -135,7 +126,7 @@ impl UserInteraction for NoUserInteraction {
&self,
key: &str,
values: &[toml::Value],
) -> Result<Vec<String>> {
) -> Result<JinjaValue> {
let suggestions = self.get_suggestions(key, values);
bail!("Replacement not selected: {key}\nSuggestions:\n{suggestions}");
}
Expand All @@ -148,15 +139,15 @@ impl UserInteraction for CliUserInteraction {
prompt_user(key, fallback)
}

fn select(&self, key: &str, values: &[toml::Value]) -> Result<String> {
fn select(&self, key: &str, values: &[toml::Value]) -> Result<JinjaValue> {
select_replacement(key, values)
}

fn select_multiple(
&self,
key: &str,
values: &[Value],
) -> Result<Vec<String>> {
) -> Result<JinjaValue> {
select_replacement_multiple(key, values)
}
}
Expand Down Expand Up @@ -190,32 +181,38 @@ fn prompt_for_date(key: &str) -> Result<Option<String>> {
Ok(res.map(formatter))
}

fn select_replacement(key: &str, values: &[Value]) -> Result<String> {
fn select_replacement(key: &str, values: &[Value]) -> Result<JinjaValue> {
let list_options = values_to_list_options(values);
let selected =
Select::new(&format!("Select value for {key}"), list_options)
.with_scorer(&|filter, _, value, _| fuzzy_match(filter, value))
.with_page_size(15)
.prompt()?;

list_option_to_string(key, values, &selected)
Ok(JinjaValue::from(list_option_to_string(
key, values, &selected,
)?))
}

fn select_replacement_multiple(
key: &str,
values: &[Value],
) -> Result<Vec<String>> {
) -> Result<JinjaValue> {
let list_options = values_to_list_options(values);
let selected =
MultiSelect::new(&format!("Select value for {key}"), list_options)
.with_scorer(&|filter, _, value, _| fuzzy_match(filter, value))
.with_page_size(15)
.prompt()?;

selected
let items: Vec<JinjaValue> = selected
.iter()
.map(|item| list_option_to_string(key, values, item))
.collect()
.map(|item| {
list_option_to_string(key, values, item).map(JinjaValue::from)
})
.collect::<Result<Vec<_>>>()?;

Ok(JinjaValue::from(items))
}

fn values_to_list_options(values: &[Value]) -> Vec<ListOption<String>> {
Expand Down
Loading