mirror of
https://github.com/mileswolfallen2/miles-web.git
synced 2026-09-08 11:23:17 +00:00
202 lines
13 KiB
HTML
202 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>CommandMove: Cut & Paste on Mac | Miles Wolf Allen</title>
|
|
<meta name="description" content="How I added true cut-and-paste to macOS Finder with CommandMove — global event taps, AppleScript, and Accessibility permissions.">
|
|
<link rel="canonical" href="https://milesallen.site/blog/command-move-devlog.html">
|
|
<meta property="og:title" content="CommandMove: Cut & Paste on Mac | Miles Wolf Allen">
|
|
<meta property="og:description" content="How I added true cut-and-paste to macOS Finder with CommandMove — global event taps, AppleScript, and Accessibility permissions.">
|
|
<meta property="og:image" content="https://milesallen.site/og.png">
|
|
<meta property="og:type" content="article">
|
|
<meta property="article:published_time" content="2026-02-15">
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "Article",
|
|
"headline": "CommandMove: Cut & Paste on Mac | Miles Wolf Allen",
|
|
"description": "How I added true cut-and-paste to macOS Finder with CommandMove — global event taps, AppleScript, and Accessibility permissions.",
|
|
"author": {
|
|
"@type": "Person",
|
|
"name": "Miles Wolf Allen",
|
|
"url": "https://milesallen.site/"
|
|
},
|
|
"datePublished": "2026-02-15",
|
|
"url": "https://milesallen.site/blog/command-move-devlog.html",
|
|
"mainEntityOfPage": "https://milesallen.site/blog/command-move-devlog.html"
|
|
}
|
|
</script>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Space+Mono:wght@400;700&family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
|
|
<link rel="icon" type="image/svg+xml" href="../favicon.svg">
|
|
<link rel="stylesheet" href="../projects/style.css">
|
|
<style>
|
|
.article-header{padding:100px 0 40px;}
|
|
.article-eyebrow{font-family:var(--font-pixel);font-size:10px;color:var(--tape);margin-bottom:12px;text-transform:uppercase;letter-spacing:.1em;}
|
|
.article-date{font-family:var(--font-mono);font-size:12px;color:var(--muted);margin-bottom:8px;}
|
|
.article-header h1{font-family:var(--font-pixel);font-size:clamp(24px,5vw,44px);line-height:1.2;margin-bottom:16px;transform:rotate(-1deg);transform-origin:left center;text-shadow:3px 3px 0 var(--accent),-2px -2px 0 var(--tape);animation:glitch 5s ease-in-out infinite;}
|
|
@keyframes glitch{0%,90%,100%{text-shadow:3px 3px 0 var(--accent),-2px -2px 0 var(--tape);}92%{text-shadow:3px 3px 0 var(--red),-2px -2px 0 var(--accent);transform:rotate(-1deg) translateX(2px);}94%{text-shadow:3px 3px 0 var(--accent),-2px -2px 0 var(--red);transform:rotate(-1deg) translateX(-2px);}}
|
|
.article-tagline{font-size:16px;color:var(--muted);max-width:600px;line-height:1.7;font-family:var(--font-mono);margin-bottom:24px;}
|
|
.article-tags{display:flex;gap:8px;flex-wrap:wrap;}
|
|
.article-tag{font-family:var(--font-pixel);font-size:8px;padding:6px 12px;background:var(--accent);color:#000;text-transform:uppercase;}
|
|
.article-body{max-width:720px;}
|
|
.article-body h2{font-family:var(--font-pixel);font-size:16px;color:var(--accent);margin:40px 0 16px;text-transform:uppercase;}
|
|
.article-body h3{font-family:var(--font-mono);font-weight:700;font-size:16px;margin:32px 0 12px;color:var(--text);}
|
|
.article-body p{font-size:14px;color:#ccc;line-height:1.9;font-family:var(--font-mono);margin-bottom:16px;}
|
|
.article-body code{background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.12);padding:2px 6px;font-size:12px;font-family:var(--font-mono);}
|
|
.article-body blockquote{border-left:3px solid var(--accent);padding:12px 20px;margin:24px 0;background:rgba(255,255,255,0.02);font-style:italic;color:var(--muted);font-family:var(--font-mono);font-size:13px;}
|
|
.article-body ul{margin:12px 0 16px 20px;font-family:var(--font-mono);font-size:13px;color:#ccc;line-height:1.8;}
|
|
.article-body ul li{margin-bottom:6px;}
|
|
.article-body ul li::marker{color:var(--accent);}
|
|
.back-link{display:inline-flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:12px;color:var(--muted);margin-top:40px;text-decoration:none;transition:color .2s;}
|
|
.back-link:hover{color:var(--accent);}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="cursor-dot" id="cursorDot"></div>
|
|
<div class="cursor-ring" id="cursorRing"></div>
|
|
<div class="progress-bar" id="progressBar"></div>
|
|
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme"></button>
|
|
<div class="shortcut-modal" id="shortcutModal">
|
|
<div class="shortcut-card">
|
|
<h3>Keyboard Shortcuts</h3>
|
|
<div class="shortcut-row"><span>Go Home</span><kbd>H</kbd></div>
|
|
<div class="shortcut-row"><span>Go to GitHub</span><kbd>G</kbd></div>
|
|
<div class="shortcut-row"><span>Toggle theme</span><kbd>T</kbd></div>
|
|
<div class="shortcut-row"><span>Show shortcuts</span><kbd>?</kbd></div>
|
|
<div class="close-hint">Press Esc or ? to close</div>
|
|
</div>
|
|
</div>
|
|
<div class="flash" id="flash"></div>
|
|
<div class="toast-stack" id="toastStack"></div>
|
|
<nav class="mobile-nav">
|
|
<a href="../index.html">Home</a>
|
|
<a href="fedl.html">FEDL</a>
|
|
<a href="omniemu.html">OmniEmu</a>
|
|
<a href="command-move.html">CommandMove</a>
|
|
<a href="https://github.com/mileswolfallen2" target="_blank">GitHub</a>
|
|
</nav>
|
|
|
|
<div class="container">
|
|
<nav>
|
|
<a href="../index.html" class="nav-logo"><span class="logo-dot"></span> milesallen</a>
|
|
<div class="nav-links">
|
|
<a href="../index.html#about">About</a>
|
|
<a href="../index.html#skills">Skills</a>
|
|
<a href="../index.html#projects">Projects</a>
|
|
<a href="../index.html#contact">Contact</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<section class="article-header">
|
|
<div class="breadcrumb">
|
|
<a href="../index.html">Home</a>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
|
<a href="../index.html#blog">Devlog</a>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
|
<span>CommandMove</span>
|
|
</div>
|
|
<div class="article-eyebrow">// DEVLOG 003</div>
|
|
<div class="article-date">2026</div>
|
|
<h1>CommandMove: Cut & Paste on Mac</h1>
|
|
<p class="article-tagline">macOS doesn't have Cmd+X for files. I fixed that. Global event taps, AppleScript, and a lot of Accessibility permissions.</p>
|
|
<div class="article-tags">
|
|
<span class="article-tag">Swift</span>
|
|
<span class="article-tag">macOS</span>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="reveal">
|
|
<div class="article-body">
|
|
<h2>The Frustration</h2>
|
|
<p>If you've ever used a Mac and tried to move files in Finder, you know the pain. You select files, press Cmd+C, navigate to a new folder, press Cmd+V... and the files are <strong>copied</strong>, not moved. The originals stay where they were.</p>
|
|
<p>On Windows, Cmd+X (or Ctrl+X) cuts files, and Cmd+V moves them. It's been that way since the 90s. macOS only supports copy-paste for files. There is no cut. I don't know why Apple decided this was a good idea, but here we are.</p>
|
|
<p>Every time I had to move files on my Mac, I'd copy them, navigate to the destination, paste, go back to the original, select the originals, and delete them. Four steps instead of two. Multiply that by hundreds of file operations and you start to lose your mind.</p>
|
|
|
|
<h2>The Solution</h2>
|
|
<p>CommandMove is a menu bar app that intercepts Cmd+X and Cmd+V in Finder and makes them do what they should have always done: cut and move files.</p>
|
|
<p>Press Cmd+X on selected files in Finder, navigate to any folder, press Cmd+V, and the files move. Just like Windows. Just like it should be.</p>
|
|
|
|
<h2>How It Works</h2>
|
|
<p>Under the hood, CommandMove uses three macOS technologies:</p>
|
|
<ul>
|
|
<li><strong>Global Event Tap:</strong> Intercepts Cmd+X and Cmd+V keystrokes while Finder is the frontmost app</li>
|
|
<li><strong>AppleScript:</strong> Queries Finder for the selected items and current folder</li>
|
|
<li><strong>FileManager:</strong> Performs the actual move operation using <code>FileManager.moveItem</code></li>
|
|
</ul>
|
|
<p>The event tap is the trickiest part. macOS is very protective about who can intercept keyboard events. You need Accessibility permissions, which means the user has to go into System Settings > Privacy & Security > Accessibility and manually enable CommandMove. macOS will prompt for this on first launch, but it's still a friction point.</p>
|
|
|
|
<h2>The Accessibility Hurdle</h2>
|
|
<p>Building for macOS means respecting the security model. Global event taps are powerful but regulated for good reason — they can theoretically capture any keystroke. Apple's solution is the Accessibility permission system, which requires explicit user consent.</p>
|
|
<p>The first time you launch CommandMove, it pops up a dialog explaining why it needs Accessibility access. Some users are wary of this, and I don't blame them. The app is open source, so anyone can verify what it does. But "please give me permission to read your keyboard" is always going to be a hard sell.</p>
|
|
|
|
<h2>Extra Features</h2>
|
|
<p>Once I had the core cut-and-paste working, I kept adding things:</p>
|
|
<ul>
|
|
<li>Right-click "Cut Files" action in Finder via a bundled Quick Action</li>
|
|
<li>Menu bar controls for cutting, pasting, clearing the clipboard, and toggling login launch</li>
|
|
<li>Auto-launch at login so it's always running</li>
|
|
<li>Conflict handling — if a file with the same name exists at the destination, it creates a numbered copy automatically</li>
|
|
</ul>
|
|
|
|
<h2>Lessons Learned</h2>
|
|
<ul>
|
|
<li>Swift is a beautiful language once you get past the learning curve</li>
|
|
<li>AppleScript is from 1993 and it shows, but it still works</li>
|
|
<li>macOS Accessibility APIs are powerful but poorly documented</li>
|
|
<li>The best utility apps solve one problem extremely well</li>
|
|
<li>Open source builds trust, especially when you're asking for Accessibility permissions</li>
|
|
</ul>
|
|
|
|
<blockquote>The simplest solutions are often the ones that should have existed from the start.</blockquote>
|
|
|
|
<a href="../index.html#blog" class="back-link">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polyline points="15 18 9 12 15 6"/></svg>
|
|
Back to Devlog
|
|
</a>
|
|
</div>
|
|
</section>
|
|
|
|
<footer>
|
|
<div class="footer-links" style="margin-bottom: 12px;">
|
|
<a href="../uses.html" style="color:var(--muted);text-decoration:none;">Uses</a>
|
|
<span style="color:var(--muted);margin:0 8px;">·</span>
|
|
<a href="../now.html" style="color:var(--muted);text-decoration:none;">Now</a>
|
|
<span style="color:var(--muted);margin:0 8px;">·</span>
|
|
<a href="../colophon.html" style="color:var(--muted);text-decoration:none;">Colophon</a>
|
|
</div>
|
|
Built by Miles · 2026 · Powered by too much coffee and the Konami code
|
|
</footer>
|
|
</div>
|
|
<script>
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('visible'); });
|
|
}, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' });
|
|
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
|
|
</script>
|
|
<script src="../projects/script.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/[email protected]/gsap.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/[email protected]/ScrollTrigger.min.js"></script>
|
|
<script>
|
|
gsap.registerPlugin(ScrollTrigger);
|
|
const heroTl = gsap.timeline({ defaults: { ease: 'power3.out' } });
|
|
heroTl
|
|
.from('.breadcrumb', { x: -30, opacity: 0, duration: 0.6 })
|
|
.from('.article-header h1', { y: 50, opacity: 0, duration: 0.8 }, '-=0.3')
|
|
.from('.article-tagline', { y: 30, opacity: 0, duration: 0.7 }, '-=0.4')
|
|
.from('.article-tag', { y: 20, opacity: 0, scale: 0.8, duration: 0.4, stagger: 0.06 }, '-=0.3');
|
|
gsap.from('nav', { y: -20, opacity: 0, duration: 0.6, ease: 'power2.out' });
|
|
document.querySelectorAll('.article-body h2').forEach(title => {
|
|
gsap.from(title, { scrollTrigger: { trigger: title, start: 'top 85%' }, y: 30, opacity: 0, duration: 0.7, ease: 'power3.out' });
|
|
});
|
|
document.querySelectorAll('.article-body p, .article-body ul, .article-body blockquote').forEach(el => {
|
|
gsap.from(el, { scrollTrigger: { trigger: el, start: 'top 88%' }, y: 20, opacity: 0, duration: 0.6, ease: 'power3.out' });
|
|
});
|
|
gsap.from('footer', { scrollTrigger: { trigger: 'footer', start: 'top 95%' }, opacity: 0, duration: 0.8 });
|
|
window.addEventListener('load', () => ScrollTrigger.refresh());
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|