fix: install.sh falls back to ~/.local/bin with PATH hint, redesign terminal section as interactive TUI simulation

This commit is contained in:
noah
2026-03-09 23:01:59 +01:00
parent b57b843459
commit b47399335a
2 changed files with 247 additions and 41 deletions
+68 -12
View File
@@ -5,7 +5,6 @@ set -e
REPO="noauf/Transmute"
BINARY="transmute"
INSTALL_DIR="/usr/local/bin"
# Detect OS
OS="$(uname -s)"
@@ -39,19 +38,20 @@ fi
ASSET="${BINARY}-${OS}-${ARCH}.${EXT}"
echo "Transmute CLI installer"
echo "======================"
echo ""
echo " Transmute CLI installer"
echo " ======================"
echo ""
echo " OS: $OS"
echo " Arch: $ARCH"
echo ""
# Get latest release tag
echo "Fetching latest release..."
echo " Fetching latest release..."
TAG=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')
if [ -z "$TAG" ]; then
echo "Error: could not determine latest release"
echo " Error: could not determine latest release"
exit 1
fi
@@ -84,26 +84,82 @@ elif [ -f "${TMP_DIR}/${BINARY}-${OS}-${ARCH}/${BINARY}" ]; then
fi
if [ -z "$BIN_PATH" ]; then
echo "Error: could not find ${BINARY} binary in archive"
echo " Error: could not find ${BINARY} binary in archive"
exit 1
fi
chmod +x "$BIN_PATH"
# Install
echo " Installing to ${INSTALL_DIR}/${BINARY}..."
if [ -w "$INSTALL_DIR" ]; then
# Determine install directory — try in order of preference
INSTALL_DIR=""
NEEDS_PATH_HINT=""
if [ -w "/usr/local/bin" ]; then
INSTALL_DIR="/usr/local/bin"
elif command -v sudo >/dev/null 2>&1; then
# Try sudo to /usr/local/bin
echo " Installing to /usr/local/bin (requires sudo)..."
if sudo mv "$BIN_PATH" "/usr/local/bin/${BINARY}" 2>/dev/null; then
sudo chmod +x "/usr/local/bin/${BINARY}"
INSTALL_DIR="/usr/local/bin"
fi
fi
# Fallback: ~/.local/bin (no sudo needed)
if [ -z "$INSTALL_DIR" ]; then
INSTALL_DIR="${HOME}/.local/bin"
mkdir -p "$INSTALL_DIR"
mv "$BIN_PATH" "${INSTALL_DIR}/${BINARY}"
chmod +x "${INSTALL_DIR}/${BINARY}"
# Check if ~/.local/bin is in PATH
case ":${PATH}:" in
*":${INSTALL_DIR}:"*) ;;
*)
NEEDS_PATH_HINT="true"
;;
esac
else
echo " (requires sudo)"
sudo mv "$BIN_PATH" "${INSTALL_DIR}/${BINARY}"
if [ -f "$BIN_PATH" ]; then
mv "$BIN_PATH" "${INSTALL_DIR}/${BINARY}"
chmod +x "${INSTALL_DIR}/${BINARY}"
fi
fi
echo ""
echo " Installed transmute $TAG to ${INSTALL_DIR}/${BINARY}"
echo ""
# If we installed to a dir not in PATH, tell the user how to fix it
if [ "$NEEDS_PATH_HINT" = "true" ]; then
SHELL_NAME="$(basename "$SHELL")"
case "$SHELL_NAME" in
zsh) RC_FILE="~/.zshrc" ;;
bash) RC_FILE="~/.bashrc" ;;
fish) RC_FILE="~/.config/fish/config.fish" ;;
*) RC_FILE="your shell config" ;;
esac
echo " To make it globally available, add ~/.local/bin to your PATH:"
echo ""
if [ "$SHELL_NAME" = "fish" ]; then
echo " fish_add_path ${INSTALL_DIR}"
else
echo " echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ${RC_FILE}"
fi
echo ""
echo " Then restart your terminal, or run:"
echo ""
if [ "$SHELL_NAME" = "fish" ]; then
echo " fish_add_path ${INSTALL_DIR}"
else
echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
fi
echo ""
fi
echo " Get started:"
echo " transmute *.png Convert all PNGs"
echo " transmute ./files/ Convert all files in a directory"
echo " transmute ./photos/ Convert all files in a directory"
echo " transmute --help Show all options"
echo ""
+179 -29
View File
@@ -585,6 +585,138 @@ function FinderWindow() {
);
}
/* ─── TUI File Rows (simulated Bubble Tea interface) ─── */
const tuiFiles = [
{ icon: '\u{1F5BC}', name: 'vacation-photo.heic', ext: 'HEIC', size: '2.4 MB', target: 'webp', category: '#f472b6', selected: true },
{ icon: '\u{1F4C4}', name: 'quarterly-report.docx', ext: 'DOCX', size: '1.8 MB', target: 'pdf', category: '#60a5fa', selected: true },
{ icon: '\u{1F3B5}', name: 'podcast-episode.flac', ext: 'FLAC', size: '48 MB', target: 'mp3', category: '#a78bfa', selected: true },
{ icon: '\u{1F4CA}', name: 'user-analytics.csv', ext: 'CSV', size: '340 KB', target: 'json', category: '#34d399', selected: true },
{ icon: '\u{1F3AC}', name: 'screen-recording.mov', ext: 'MOV', size: '126 MB', target: 'mp4', category: '#fb923c', selected: true },
{ icon: '\u{1F524}', name: 'brand-font.ttf', ext: 'TTF', size: '420 KB', target: 'woff2', category: '#2dd4bf', selected: false },
];
type TUIStatus = 'idle' | 'converting' | 'done';
function TUIFileRows() {
const [cursorIdx, setCursorIdx] = useState(0);
const [statuses, setStatuses] = useState<TUIStatus[]>(Array(tuiFiles.length).fill('idle'));
const cycleRef = useRef(0);
useEffect(() => {
// Animate: cycle cursor down, then run a conversion animation, then reset
const cycle = () => {
const thisCycle = ++cycleRef.current;
// Phase 1: Cursor moves down the list
tuiFiles.forEach((_, i) => {
setTimeout(() => {
if (cycleRef.current !== thisCycle) return;
setCursorIdx(i);
}, i * 400);
});
// Phase 2: Start conversion sequence after cursor finishes
const convStart = tuiFiles.length * 400 + 600;
tuiFiles.forEach((f, i) => {
if (!f.selected) return;
setTimeout(() => {
if (cycleRef.current !== thisCycle) return;
setStatuses(prev => { const n = [...prev]; n[i] = 'converting'; return n; });
}, convStart + i * 500);
setTimeout(() => {
if (cycleRef.current !== thisCycle) return;
setStatuses(prev => { const n = [...prev]; n[i] = 'done'; return n; });
}, convStart + i * 500 + 800);
});
// Phase 3: Reset after all done
const totalTime = convStart + tuiFiles.length * 500 + 2500;
setTimeout(() => {
if (cycleRef.current !== thisCycle) return;
setStatuses(Array(tuiFiles.length).fill('idle'));
setCursorIdx(0);
cycle();
}, totalTime);
};
cycle();
return () => { cycleRef.current++; };
}, []);
return (
<div>
{tuiFiles.map((f, i) => {
const isCursor = i === cursorIdx;
const status = statuses[i];
return (
<div
key={f.name}
className="flex items-center px-4 py-[3px] transition-colors duration-150"
style={{ background: isCursor ? '#f8f0e6' : 'transparent' }}
>
{/* Cursor + checkbox */}
<div className="w-6 flex-shrink-0 text-[12px]">
{isCursor ? (
<span className="text-[#f472b6] font-bold">{'>'}</span>
) : (
<span>{' '}</span>
)}
</div>
<div className="w-4 flex-shrink-0 text-[12px]">
{f.selected ? (
<span className="text-[#34d399]">{'\u25CF'}</span>
) : (
<span className="text-[#bfa98a]">{'\u25CB'}</span>
)}
</div>
{/* Icon + ext badge + name */}
<div className="flex-1 flex items-center gap-1.5 min-w-0 pl-1">
<span className="text-[11px]">{f.icon}</span>
<span className="font-bold text-[10px]" style={{ color: f.category }}>{f.ext}</span>
<span className={`text-[12px] truncate ${isCursor ? 'font-bold text-[#2d1f14]' : 'text-[#2d1f14]'}`}>
{f.name}
</span>
</div>
{/* Size */}
<div className="w-[72px] text-right text-[#bfa98a] text-[11px] flex-shrink-0 hidden sm:block">
{f.size}
</div>
{/* Format selector */}
<div className="w-[100px] text-center flex-shrink-0">
{isCursor ? (
<span>
<span className="text-[#bfa98a]">{'< '}</span>
<span className="text-[#f472b6] font-bold">{f.target}</span>
<span className="text-[#bfa98a]">{' >'}</span>
</span>
) : (
<span className="text-[#2d1f14]">{f.target}</span>
)}
</div>
{/* Status */}
<div className="w-[80px] text-center flex-shrink-0">
{status === 'idle' && (
<span className="text-[#bfa98a] italic">idle</span>
)}
{status === 'converting' && (
<span className="text-[#f472b6] font-bold animate-pulse">converting...</span>
)}
{status === 'done' && (
<span className="text-[#34d399] font-bold">done</span>
)}
</div>
</div>
);
})}
</div>
);
}
/* ─── Main Page ─── */
export default function LandingPage() {
@@ -963,12 +1095,12 @@ export default function LandingPage() {
Prefer the command line?
</h2>
<p className="text-[17px] text-text-mid leading-relaxed max-w-[520px]">
Transmute has a full-featured CLI with an interactive TUI. Batch convert files, use glob patterns, pipe into scripts.
A full interactive TUI. Navigate files, pick formats, and batch convert without leaving your terminal.
</p>
</motion.div>
<motion.div
className="w-full max-w-[640px]"
className="w-full max-w-[720px]"
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
@@ -984,40 +1116,58 @@ export default function LandingPage() {
<div className="w-[11px] h-[11px] rounded-full bg-[#28c840] border border-[#1aab29]/40" />
</div>
<div className="flex-1 text-center">
<span className="text-[12px] font-mono text-[#888]">Terminal</span>
<span className="text-[12px] font-mono text-[#888]">transmute ./photos/</span>
</div>
</div>
{/* Terminal body */}
<div className="bg-[#1a1a1a] px-5 py-5 font-mono text-[13px] leading-relaxed">
{/* Install command */}
<div className="flex items-start gap-2 mb-4">
<span className="text-[#34d399] select-none font-bold">$</span>
<div>
<span className="text-[#e2e2e2]">curl -fsSL </span>
<span className="text-[#60a5fa]">https://raw.githubusercontent.com/noauf/Transmute/main/install.sh</span>
<span className="text-[#e2e2e2]"> | </span>
<span className="text-[#fb923c]">sh</span>
{/* TUI body — simulated Bubble Tea interface with cream bg */}
<div className="bg-[#fdf6ef] font-mono text-[12px] leading-[1.6] select-none">
{/* TUI title bar */}
<div className="flex items-center justify-between px-4 py-2">
<div className="flex items-center gap-2">
<span className="text-[#f472b6] font-bold text-[13px]">transmute</span>
<span className="text-[#8b7355]">6 files {'\u00B7'} 5 selected</span>
</div>
<span className="text-[#bfa98a] italic">? help</span>
</div>
{/* Simulated output */}
<div className="text-[#666] text-[12px] mb-4 pl-4 border-l-2 border-[#333]">
<div>Transmute CLI installer</div>
<div> OS: darwin Arch: arm64</div>
<div> Latest version: v0.1.0</div>
<div className="text-[#34d399]"> Installed transmute v0.1.0</div>
{/* Divider */}
<div className="px-4 text-[#e8e0d4]">{'\u2500'.repeat(80)}</div>
{/* Column header */}
<div className="flex items-center px-4 py-1 text-[11px] text-[#8b7355]">
<div className="flex-1 pl-6">Name</div>
<div className="w-[72px] text-right hidden sm:block">Size</div>
<div className="w-[100px] text-center">Convert to</div>
<div className="w-[80px] text-center">Status</div>
</div>
{/* Usage examples */}
<div className="flex items-start gap-2 mb-1">
<span className="text-[#34d399] select-none font-bold">$</span>
<span className="text-[#e2e2e2]">transmute <span className="text-[#f472b6]">*.png</span></span>
{/* File rows */}
<TUIFileRows />
{/* Divider */}
<div className="px-4 text-[#e8e0d4]">{'\u2500'.repeat(80)}</div>
{/* Bottom bar */}
<div className="flex items-center justify-between px-4 py-2">
<span className="bg-[#f472b6] text-white font-bold text-[11px] px-3 py-0.5 rounded">
Convert 5 files [c]
</span>
<span className="text-[#bfa98a] italic text-[11px] hidden sm:inline">
up/down navigate left/right format space select a all q quit
</span>
<span className="text-[#bfa98a] italic text-[11px] sm:hidden">
{'\u2191\u2193'} nav {'\u2190\u2192'} fmt spc sel q quit
</span>
</div>
<div className="text-[#666] text-[12px] mb-3 pl-4">Convert all PNGs in current directory</div>
<div className="flex items-start gap-2 mb-1">
<span className="text-[#34d399] select-none font-bold">$</span>
<span className="text-[#e2e2e2]">transmute <span className="text-[#f472b6]">./photos/</span> <span className="text-[#a78bfa]">-d</span> <span className="text-[#fb923c]">./output/</span></span>
</div>
<div className="text-[#666] text-[12px] pl-4">Batch convert a whole directory</div>
</div>
</div>
{/* Install command below the TUI */}
<div className="mt-6 rounded-xl overflow-hidden border border-[#2d2d2d] bg-[#1a1a1a]">
<div className="flex items-center gap-3 px-4 py-3 font-mono text-[13px]">
<span className="text-[#34d399] select-none font-bold">$</span>
<span className="text-[#e2e2e2] break-all">curl -fsSL <span className="text-[#60a5fa]">https://raw.githubusercontent.com/noauf/Transmute/main/install.sh</span> | <span className="text-[#fb923c]">sh</span></span>
</div>
</div>
</motion.div>