2025-08-25 16:39:42 -07:00
|
|
|
use std::path::Path;
|
2025-08-22 18:05:43 +01:00
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use tempfile::Builder;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub enum PasteImageError {
|
|
|
|
|
ClipboardUnavailable(String),
|
|
|
|
|
NoImage(String),
|
|
|
|
|
EncodeFailed(String),
|
|
|
|
|
IoError(String),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::fmt::Display for PasteImageError {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
match self {
|
|
|
|
|
PasteImageError::ClipboardUnavailable(msg) => write!(f, "clipboard unavailable: {msg}"),
|
|
|
|
|
PasteImageError::NoImage(msg) => write!(f, "no image on clipboard: {msg}"),
|
|
|
|
|
PasteImageError::EncodeFailed(msg) => write!(f, "could not encode image: {msg}"),
|
|
|
|
|
PasteImageError::IoError(msg) => write!(f, "io error: {msg}"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
impl std::error::Error for PasteImageError {}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
pub enum EncodedImageFormat {
|
|
|
|
|
Png,
|
2025-08-25 16:39:42 -07:00
|
|
|
Jpeg,
|
|
|
|
|
Other,
|
2025-08-22 18:05:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl EncodedImageFormat {
|
|
|
|
|
pub fn label(self) -> &'static str {
|
|
|
|
|
match self {
|
|
|
|
|
EncodedImageFormat::Png => "PNG",
|
2025-08-25 16:39:42 -07:00
|
|
|
EncodedImageFormat::Jpeg => "JPEG",
|
|
|
|
|
EncodedImageFormat::Other => "IMG",
|
2025-08-22 18:05:43 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct PastedImageInfo {
|
|
|
|
|
pub width: u32,
|
|
|
|
|
pub height: u32,
|
|
|
|
|
pub encoded_format: EncodedImageFormat, // Always PNG for now.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Capture image from system clipboard, encode to PNG, and return bytes + info.
|
2025-09-03 15:36:40 +09:00
|
|
|
#[cfg(not(target_os = "android"))]
|
2025-08-22 18:05:43 +01:00
|
|
|
pub fn paste_image_as_png() -> Result<(Vec<u8>, PastedImageInfo), PasteImageError> {
|
2025-09-08 09:31:42 -07:00
|
|
|
let _span = tracing::debug_span!("paste_image_as_png").entered();
|
2025-08-22 18:05:43 +01:00
|
|
|
tracing::debug!("attempting clipboard image read");
|
|
|
|
|
let mut cb = arboard::Clipboard::new()
|
|
|
|
|
.map_err(|e| PasteImageError::ClipboardUnavailable(e.to_string()))?;
|
2025-09-08 09:31:42 -07:00
|
|
|
// Sometimes images on the clipboard come as files (e.g. when copy/pasting from
|
|
|
|
|
// Finder), sometimes they come as image data (e.g. when pasting from Chrome).
|
|
|
|
|
// Accept both, and prefer files if both are present.
|
|
|
|
|
let files = cb
|
|
|
|
|
.get()
|
|
|
|
|
.file_list()
|
|
|
|
|
.map_err(|e| PasteImageError::ClipboardUnavailable(e.to_string()));
|
|
|
|
|
let dyn_img = if let Some(img) = files
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.find_map(|f| image::open(f).ok())
|
|
|
|
|
{
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
"clipboard image opened from file: {}x{}",
|
|
|
|
|
img.width(),
|
|
|
|
|
img.height()
|
|
|
|
|
);
|
|
|
|
|
img
|
|
|
|
|
} else {
|
|
|
|
|
let _span = tracing::debug_span!("get_image").entered();
|
|
|
|
|
let img = cb
|
|
|
|
|
.get_image()
|
|
|
|
|
.map_err(|e| PasteImageError::NoImage(e.to_string()))?;
|
|
|
|
|
let w = img.width as u32;
|
|
|
|
|
let h = img.height as u32;
|
|
|
|
|
tracing::debug!("clipboard image opened from image: {}x{}", w, h);
|
2025-08-22 18:05:43 +01:00
|
|
|
|
2025-09-08 09:31:42 -07:00
|
|
|
let Some(rgba_img) = image::RgbaImage::from_raw(w, h, img.bytes.into_owned()) else {
|
|
|
|
|
return Err(PasteImageError::EncodeFailed("invalid RGBA buffer".into()));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
image::DynamicImage::ImageRgba8(rgba_img)
|
2025-08-22 18:05:43 +01:00
|
|
|
};
|
2025-09-08 09:31:42 -07:00
|
|
|
|
|
|
|
|
let mut png: Vec<u8> = Vec::new();
|
2025-08-22 18:05:43 +01:00
|
|
|
{
|
2025-09-08 09:31:42 -07:00
|
|
|
let span =
|
|
|
|
|
tracing::debug_span!("encode_image", byte_length = tracing::field::Empty).entered();
|
2025-08-22 18:05:43 +01:00
|
|
|
let mut cursor = std::io::Cursor::new(&mut png);
|
|
|
|
|
dyn_img
|
|
|
|
|
.write_to(&mut cursor, image::ImageFormat::Png)
|
|
|
|
|
.map_err(|e| PasteImageError::EncodeFailed(e.to_string()))?;
|
2025-09-08 09:31:42 -07:00
|
|
|
span.record("byte_length", png.len());
|
2025-08-22 18:05:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok((
|
|
|
|
|
png,
|
|
|
|
|
PastedImageInfo {
|
2025-09-08 09:31:42 -07:00
|
|
|
width: dyn_img.width(),
|
|
|
|
|
height: dyn_img.height(),
|
2025-08-22 18:05:43 +01:00
|
|
|
encoded_format: EncodedImageFormat::Png,
|
|
|
|
|
},
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-03 15:36:40 +09:00
|
|
|
/// Android/Termux does not support arboard; return a clear error.
|
|
|
|
|
#[cfg(target_os = "android")]
|
|
|
|
|
pub fn paste_image_as_png() -> Result<(Vec<u8>, PastedImageInfo), PasteImageError> {
|
|
|
|
|
Err(PasteImageError::ClipboardUnavailable(
|
|
|
|
|
"clipboard image paste is unsupported on Android".into(),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 18:05:43 +01:00
|
|
|
/// Convenience: write to a temp file and return its path + info.
|
2025-09-03 15:36:40 +09:00
|
|
|
#[cfg(not(target_os = "android"))]
|
2025-08-22 18:05:43 +01:00
|
|
|
pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImageError> {
|
|
|
|
|
let (png, info) = paste_image_as_png()?;
|
|
|
|
|
// Create a unique temporary file with a .png suffix to avoid collisions.
|
|
|
|
|
let tmp = Builder::new()
|
feat: Complete LLMX v0.1.0 - Rebrand from Codex with LiteLLM Integration
This release represents a comprehensive transformation of the codebase from Codex to LLMX,
enhanced with LiteLLM integration to support 100+ LLM providers through a unified API.
## Major Changes
### Phase 1: Repository & Infrastructure Setup
- Established new repository structure and branching strategy
- Created comprehensive project documentation (CLAUDE.md, LITELLM-SETUP.md)
- Set up development environment and tooling configuration
### Phase 2: Rust Workspace Transformation
- Renamed all Rust crates from `codex-*` to `llmx-*` (30+ crates)
- Updated package names, binary names, and workspace members
- Renamed core modules: codex.rs → llmx.rs, codex_delegate.rs → llmx_delegate.rs
- Updated all internal references, imports, and type names
- Renamed directories: codex-rs/ → llmx-rs/, codex-backend-openapi-models/ → llmx-backend-openapi-models/
- Fixed all Rust compilation errors after mass rename
### Phase 3: LiteLLM Integration
- Integrated LiteLLM for multi-provider LLM support (Anthropic, OpenAI, Azure, Google AI, AWS Bedrock, etc.)
- Implemented OpenAI-compatible Chat Completions API support
- Added model family detection and provider-specific handling
- Updated authentication to support LiteLLM API keys
- Renamed environment variables: OPENAI_BASE_URL → LLMX_BASE_URL
- Added LLMX_API_KEY for unified authentication
- Enhanced error handling for Chat Completions API responses
- Implemented fallback mechanisms between Responses API and Chat Completions API
### Phase 4: TypeScript/Node.js Components
- Renamed npm package: @codex/codex-cli → @valknar/llmx
- Updated TypeScript SDK to use new LLMX APIs and endpoints
- Fixed all TypeScript compilation and linting errors
- Updated SDK tests to support both API backends
- Enhanced mock server to handle multiple API formats
- Updated build scripts for cross-platform packaging
### Phase 5: Configuration & Documentation
- Updated all configuration files to use LLMX naming
- Rewrote README and documentation for LLMX branding
- Updated config paths: ~/.codex/ → ~/.llmx/
- Added comprehensive LiteLLM setup guide
- Updated all user-facing strings and help text
- Created release plan and migration documentation
### Phase 6: Testing & Validation
- Fixed all Rust tests for new naming scheme
- Updated snapshot tests in TUI (36 frame files)
- Fixed authentication storage tests
- Updated Chat Completions payload and SSE tests
- Fixed SDK tests for new API endpoints
- Ensured compatibility with Claude Sonnet 4.5 model
- Fixed test environment variables (LLMX_API_KEY, LLMX_BASE_URL)
### Phase 7: Build & Release Pipeline
- Updated GitHub Actions workflows for LLMX binary names
- Fixed rust-release.yml to reference llmx-rs/ instead of codex-rs/
- Updated CI/CD pipelines for new package names
- Made Apple code signing optional in release workflow
- Enhanced npm packaging resilience for partial platform builds
- Added Windows sandbox support to workspace
- Updated dotslash configuration for new binary names
### Phase 8: Final Polish
- Renamed all assets (.github images, labels, templates)
- Updated VSCode and DevContainer configurations
- Fixed all clippy warnings and formatting issues
- Applied cargo fmt and prettier formatting across codebase
- Updated issue templates and pull request templates
- Fixed all remaining UI text references
## Technical Details
**Breaking Changes:**
- Binary name changed from `codex` to `llmx`
- Config directory changed from `~/.codex/` to `~/.llmx/`
- Environment variables renamed (CODEX_* → LLMX_*)
- npm package renamed to `@valknar/llmx`
**New Features:**
- Support for 100+ LLM providers via LiteLLM
- Unified authentication with LLMX_API_KEY
- Enhanced model provider detection and handling
- Improved error handling and fallback mechanisms
**Files Changed:**
- 578 files modified across Rust, TypeScript, and documentation
- 30+ Rust crates renamed and updated
- Complete rebrand of UI, CLI, and documentation
- All tests updated and passing
**Dependencies:**
- Updated Cargo.lock with new package names
- Updated npm dependencies in llmx-cli
- Enhanced OpenAPI models for LLMX backend
This release establishes LLMX as a standalone project with comprehensive LiteLLM
integration, maintaining full backward compatibility with existing functionality
while opening support for a wide ecosystem of LLM providers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Sebastian Krüger <support@pivoine.art>
2025-11-12 20:40:44 +01:00
|
|
|
.prefix("llmx-clipboard-")
|
2025-08-22 18:05:43 +01:00
|
|
|
.suffix(".png")
|
|
|
|
|
.tempfile()
|
|
|
|
|
.map_err(|e| PasteImageError::IoError(e.to_string()))?;
|
|
|
|
|
std::fs::write(tmp.path(), &png).map_err(|e| PasteImageError::IoError(e.to_string()))?;
|
|
|
|
|
// Persist the file (so it remains after the handle is dropped) and return its PathBuf.
|
|
|
|
|
let (_file, path) = tmp
|
|
|
|
|
.keep()
|
|
|
|
|
.map_err(|e| PasteImageError::IoError(e.error.to_string()))?;
|
|
|
|
|
Ok((path, info))
|
|
|
|
|
}
|
2025-08-25 16:39:42 -07:00
|
|
|
|
2025-09-03 15:36:40 +09:00
|
|
|
#[cfg(target_os = "android")]
|
|
|
|
|
pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImageError> {
|
|
|
|
|
// Keep error consistent with paste_image_as_png.
|
|
|
|
|
Err(PasteImageError::ClipboardUnavailable(
|
|
|
|
|
"clipboard image paste is unsupported on Android".into(),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-25 16:39:42 -07:00
|
|
|
/// Normalize pasted text that may represent a filesystem path.
|
|
|
|
|
///
|
|
|
|
|
/// Supports:
|
|
|
|
|
/// - `file://` URLs (converted to local paths)
|
|
|
|
|
/// - Windows/UNC paths
|
|
|
|
|
/// - shell-escaped single paths (via `shlex`)
|
|
|
|
|
pub fn normalize_pasted_path(pasted: &str) -> Option<PathBuf> {
|
|
|
|
|
let pasted = pasted.trim();
|
|
|
|
|
|
|
|
|
|
// file:// URL → filesystem path
|
|
|
|
|
if let Ok(url) = url::Url::parse(pasted)
|
|
|
|
|
&& url.scheme() == "file"
|
|
|
|
|
{
|
|
|
|
|
return url.to_file_path().ok();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: We'll improve the implementation/unit tests over time, as appropriate.
|
feat: Complete LLMX v0.1.0 - Rebrand from Codex with LiteLLM Integration
This release represents a comprehensive transformation of the codebase from Codex to LLMX,
enhanced with LiteLLM integration to support 100+ LLM providers through a unified API.
## Major Changes
### Phase 1: Repository & Infrastructure Setup
- Established new repository structure and branching strategy
- Created comprehensive project documentation (CLAUDE.md, LITELLM-SETUP.md)
- Set up development environment and tooling configuration
### Phase 2: Rust Workspace Transformation
- Renamed all Rust crates from `codex-*` to `llmx-*` (30+ crates)
- Updated package names, binary names, and workspace members
- Renamed core modules: codex.rs → llmx.rs, codex_delegate.rs → llmx_delegate.rs
- Updated all internal references, imports, and type names
- Renamed directories: codex-rs/ → llmx-rs/, codex-backend-openapi-models/ → llmx-backend-openapi-models/
- Fixed all Rust compilation errors after mass rename
### Phase 3: LiteLLM Integration
- Integrated LiteLLM for multi-provider LLM support (Anthropic, OpenAI, Azure, Google AI, AWS Bedrock, etc.)
- Implemented OpenAI-compatible Chat Completions API support
- Added model family detection and provider-specific handling
- Updated authentication to support LiteLLM API keys
- Renamed environment variables: OPENAI_BASE_URL → LLMX_BASE_URL
- Added LLMX_API_KEY for unified authentication
- Enhanced error handling for Chat Completions API responses
- Implemented fallback mechanisms between Responses API and Chat Completions API
### Phase 4: TypeScript/Node.js Components
- Renamed npm package: @codex/codex-cli → @valknar/llmx
- Updated TypeScript SDK to use new LLMX APIs and endpoints
- Fixed all TypeScript compilation and linting errors
- Updated SDK tests to support both API backends
- Enhanced mock server to handle multiple API formats
- Updated build scripts for cross-platform packaging
### Phase 5: Configuration & Documentation
- Updated all configuration files to use LLMX naming
- Rewrote README and documentation for LLMX branding
- Updated config paths: ~/.codex/ → ~/.llmx/
- Added comprehensive LiteLLM setup guide
- Updated all user-facing strings and help text
- Created release plan and migration documentation
### Phase 6: Testing & Validation
- Fixed all Rust tests for new naming scheme
- Updated snapshot tests in TUI (36 frame files)
- Fixed authentication storage tests
- Updated Chat Completions payload and SSE tests
- Fixed SDK tests for new API endpoints
- Ensured compatibility with Claude Sonnet 4.5 model
- Fixed test environment variables (LLMX_API_KEY, LLMX_BASE_URL)
### Phase 7: Build & Release Pipeline
- Updated GitHub Actions workflows for LLMX binary names
- Fixed rust-release.yml to reference llmx-rs/ instead of codex-rs/
- Updated CI/CD pipelines for new package names
- Made Apple code signing optional in release workflow
- Enhanced npm packaging resilience for partial platform builds
- Added Windows sandbox support to workspace
- Updated dotslash configuration for new binary names
### Phase 8: Final Polish
- Renamed all assets (.github images, labels, templates)
- Updated VSCode and DevContainer configurations
- Fixed all clippy warnings and formatting issues
- Applied cargo fmt and prettier formatting across codebase
- Updated issue templates and pull request templates
- Fixed all remaining UI text references
## Technical Details
**Breaking Changes:**
- Binary name changed from `codex` to `llmx`
- Config directory changed from `~/.codex/` to `~/.llmx/`
- Environment variables renamed (CODEX_* → LLMX_*)
- npm package renamed to `@valknar/llmx`
**New Features:**
- Support for 100+ LLM providers via LiteLLM
- Unified authentication with LLMX_API_KEY
- Enhanced model provider detection and handling
- Improved error handling and fallback mechanisms
**Files Changed:**
- 578 files modified across Rust, TypeScript, and documentation
- 30+ Rust crates renamed and updated
- Complete rebrand of UI, CLI, and documentation
- All tests updated and passing
**Dependencies:**
- Updated Cargo.lock with new package names
- Updated npm dependencies in llmx-cli
- Enhanced OpenAPI models for LLMX backend
This release establishes LLMX as a standalone project with comprehensive LiteLLM
integration, maintaining full backward compatibility with existing functionality
while opening support for a wide ecosystem of LLM providers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Sebastian Krüger <support@pivoine.art>
2025-11-12 20:40:44 +01:00
|
|
|
// Possibly use typed-path: https://github.com/valknar/llmx/pull/2567/commits/3cc92b78e0a1f94e857cf4674d3a9db918ed352e
|
2025-08-25 16:39:42 -07:00
|
|
|
//
|
|
|
|
|
// Detect unquoted Windows paths and bypass POSIX shlex which
|
|
|
|
|
// treats backslashes as escapes (e.g., C:\Users\Alice\file.png).
|
|
|
|
|
// Also handles UNC paths (\\server\share\path).
|
|
|
|
|
let looks_like_windows_path = {
|
|
|
|
|
// Drive letter path: C:\ or C:/
|
|
|
|
|
let drive = pasted
|
|
|
|
|
.chars()
|
|
|
|
|
.next()
|
|
|
|
|
.map(|c| c.is_ascii_alphabetic())
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
&& pasted.get(1..2) == Some(":")
|
|
|
|
|
&& pasted
|
|
|
|
|
.get(2..3)
|
|
|
|
|
.map(|s| s == "\\" || s == "/")
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
// UNC path: \\server\share
|
|
|
|
|
let unc = pasted.starts_with("\\\\");
|
|
|
|
|
drive || unc
|
|
|
|
|
};
|
|
|
|
|
if looks_like_windows_path {
|
|
|
|
|
return Some(PathBuf::from(pasted));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// shell-escaped single path → unescaped
|
|
|
|
|
let parts: Vec<String> = shlex::Shlex::new(pasted).collect();
|
|
|
|
|
if parts.len() == 1 {
|
|
|
|
|
return parts.into_iter().next().map(PathBuf::from);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Infer an image format for the provided path based on its extension.
|
|
|
|
|
pub fn pasted_image_format(path: &Path) -> EncodedImageFormat {
|
|
|
|
|
match path
|
|
|
|
|
.extension()
|
|
|
|
|
.and_then(|e| e.to_str())
|
2025-09-22 20:30:16 +01:00
|
|
|
.map(str::to_ascii_lowercase)
|
2025-08-25 16:39:42 -07:00
|
|
|
.as_deref()
|
|
|
|
|
{
|
|
|
|
|
Some("png") => EncodedImageFormat::Png,
|
|
|
|
|
Some("jpg") | Some("jpeg") => EncodedImageFormat::Jpeg,
|
|
|
|
|
_ => EncodedImageFormat::Other,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod pasted_paths_tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[cfg(not(windows))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn normalize_file_url() {
|
|
|
|
|
let input = "file:///tmp/example.png";
|
|
|
|
|
let result = normalize_pasted_path(input).expect("should parse file URL");
|
|
|
|
|
assert_eq!(result, PathBuf::from("/tmp/example.png"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn normalize_file_url_windows() {
|
|
|
|
|
let input = r"C:\Temp\example.png";
|
|
|
|
|
let result = normalize_pasted_path(input).expect("should parse file URL");
|
|
|
|
|
assert_eq!(result, PathBuf::from(r"C:\Temp\example.png"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn normalize_shell_escaped_single_path() {
|
|
|
|
|
let input = "/home/user/My\\ File.png";
|
|
|
|
|
let result = normalize_pasted_path(input).expect("should unescape shell-escaped path");
|
|
|
|
|
assert_eq!(result, PathBuf::from("/home/user/My File.png"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn normalize_simple_quoted_path_fallback() {
|
|
|
|
|
let input = "\"/home/user/My File.png\"";
|
|
|
|
|
let result = normalize_pasted_path(input).expect("should trim simple quotes");
|
|
|
|
|
assert_eq!(result, PathBuf::from("/home/user/My File.png"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn normalize_single_quoted_unix_path() {
|
|
|
|
|
let input = "'/home/user/My File.png'";
|
|
|
|
|
let result = normalize_pasted_path(input).expect("should trim single quotes via shlex");
|
|
|
|
|
assert_eq!(result, PathBuf::from("/home/user/My File.png"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn normalize_multiple_tokens_returns_none() {
|
|
|
|
|
// Two tokens after shell splitting → not a single path
|
|
|
|
|
let input = "/home/user/a\\ b.png /home/user/c.png";
|
|
|
|
|
let result = normalize_pasted_path(input);
|
|
|
|
|
assert!(result.is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn pasted_image_format_png_jpeg_unknown() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
pasted_image_format(Path::new("/a/b/c.PNG")),
|
|
|
|
|
EncodedImageFormat::Png
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
pasted_image_format(Path::new("/a/b/c.jpg")),
|
|
|
|
|
EncodedImageFormat::Jpeg
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
pasted_image_format(Path::new("/a/b/c.JPEG")),
|
|
|
|
|
EncodedImageFormat::Jpeg
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
pasted_image_format(Path::new("/a/b/c")),
|
|
|
|
|
EncodedImageFormat::Other
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
pasted_image_format(Path::new("/a/b/c.webp")),
|
|
|
|
|
EncodedImageFormat::Other
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn normalize_single_quoted_windows_path() {
|
|
|
|
|
let input = r"'C:\\Users\\Alice\\My File.jpeg'";
|
|
|
|
|
let result =
|
|
|
|
|
normalize_pasted_path(input).expect("should trim single quotes on windows path");
|
|
|
|
|
assert_eq!(result, PathBuf::from(r"C:\\Users\\Alice\\My File.jpeg"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn normalize_unquoted_windows_path_with_spaces() {
|
|
|
|
|
let input = r"C:\\Users\\Alice\\My Pictures\\example image.png";
|
|
|
|
|
let result = normalize_pasted_path(input).expect("should accept unquoted windows path");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result,
|
|
|
|
|
PathBuf::from(r"C:\\Users\\Alice\\My Pictures\\example image.png")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn normalize_unc_windows_path() {
|
|
|
|
|
let input = r"\\\\server\\share\\folder\\file.jpg";
|
|
|
|
|
let result = normalize_pasted_path(input).expect("should accept UNC windows path");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result,
|
|
|
|
|
PathBuf::from(r"\\\\server\\share\\folder\\file.jpg")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn pasted_image_format_with_windows_style_paths() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
pasted_image_format(Path::new(r"C:\\a\\b\\c.PNG")),
|
|
|
|
|
EncodedImageFormat::Png
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
pasted_image_format(Path::new(r"C:\\a\\b\\c.jpeg")),
|
|
|
|
|
EncodedImageFormat::Jpeg
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
pasted_image_format(Path::new(r"C:\\a\\b\\noext")),
|
|
|
|
|
EncodedImageFormat::Other
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|