콘텐츠로 이동

cmux 대안 Hyprland 개발 환경 치트시트

환경: Arch Linux, Hyprland, Kitty, Lazygit, Yazi, Chromium (CDP)

hyprmux는 cmux의 워크플로우(터미널 분할창, 브라우저 프리뷰, AI 에이전트 브라우징 제어)를 리눅스 환경에 맞춰 재해석하고, Google Antigravity CLI(agy)를 중심으로 구동되는 Wayland/Hyprland 네이티브 경량 통합 개발 워크스페이스입니다.

무거운 일체형 일렉트론 애플리케이션 대신 리눅스의 각 최고 성능 도구(Hyprland, Kitty, Lazygit, Yazi, Chromium)를 표준 IPC(Unix 소켓, CDP, Wayland Dispatcher)로 유기적으로 결합합니다.

flowchart TD
    subgraph HYPR[Hyprland 활성 가상 작업 공간]
        subgraph TAB[Hyprland 탭 그룹 :: Super + Tab 전환]
            subgraph KWIN[Kitty 3분할 윈도우]
                direction TB
                subgraph LEFT[좌측 60%]
                    A[AGENT / 개발 쉘<br/>AI 에이전트 실행 및 메인 작업]
                end
                subgraph RIGHT[우측 40%]
                    direction TB
                    B[LAZYGIT 상단 50%<br/>Git 상태 추적 및 커밋]
                    C[YAZI 하단 50%<br/>파일 탐색 및 이미지 프리뷰]
                end
            end
            subgraph CWIN[Chromium 윈도우]
                D[독립 프로필 브라우저<br/>CDP 원격 디버깅 포트 9222]
            end
        end
    end

    A -->|CDP 웹 자동화 및 실시간 브라우징 제어| D
    B -->|작업 트리 변경 사항 감지| A
    C -->|작업 디렉터리 파일 조작| A

AI 에이전트와 사용자는 좌측 메인 쉘(AGENT)에서 코드를 조작하고, 우측 분할 영역(LAZYGIT, YAZI)에서 변경 사항과 파일 구조를 즉각 확인합니다. 동시에 백그라운드로 실행된 Chromium은 CDP(포트 9222)를 열어 에이전트가 브라우저를 직접 제어할 수 있게 하며, Hyprland 탭 그룹으로 묶여 단축키 하나로 터미널과 브라우저 화면을 전환합니다.

구성 요소 역할 연동 방식
Hyprland 윈도우 레이아웃 및 탭 그룹화 hyprctl dispatch IPC로 Kitty와 Chromium을 단일 탭 그룹으로 병합 (Super + Tab)
Kitty 고성능 터미널 3분할 멀티플렉싱 splits 레이아웃 및 Unix 소켓(/tmp/hyprmux.sock) 기반 원격 제어
AGENT (좌측 60%) AI 코딩 에이전트 쉘 & 메인 개발 콘솔 agy(Antigravity CLI) 실행 및 빌드/테스트 메인 콘솔
LAZYGIT (우측 상단 20%) Git 워크트리 변경 추적 및 커밋 관리 변경 파일의 실시간 diff 시각화 및 키 입력 기반 스테이징
YAZI (우측 하단 20%) 고속 파일 트리 탐색 및 에셋 프리뷰 비동기 I/O 파일 관리, 텍스트 및 이미지 인라인 미리보기
Chromium AI 에이전트 웹 브라우징 & 라이브 프리뷰 독립 프로필(~/.config/chromium-hyprmux)과 CDP 포트(9222) 원격 제어

1. 패키지 설치

sudo pacman -Syu hyprland kitty lazygit yazi chromium jq

2. 설정 파일

~/.config/kitty/hyprmux.session

Kitty 터미널의 3분할(좌측 60% 개발 쉘, 우측 상단 20% Lazygit, 우측 하단 20% Yazi) 레이아웃을 정의합니다.

layout splits

# 좌측 60%: 메인 에이전트/개발 쉘
launch --location=vsplit --bias=60 --title="AGENT"

# 우측 40% 상단 (50%): lazygit (스마트 Git 런처 적용)
launch --location=vsplit --bias=40 --title="LAZYGIT" hyprmux-lazygit

# 우측 40% 하단 (50%): yazi (우측 lazygit 공간에서 수평 분할)
launch --location=hsplit --bias=50 --title="YAZI" yazi

# 시작 포커스는 메인 에이전트 창으로 맞춤
focus_matching_window title:^AGENT$

~/bin/hyprmux-lazygit

Git 저장소 외부에서 Lazygit이 즉시 오류를 내며 종료되지 않도록 주변 Git 프로젝트를 탐색해 선택할 수 있는 래퍼 스크립트입니다. 실행 권한(chmod +x)을 부여합니다.

#!/usr/bin/env bash
export PATH="$HOME/bin:$HOME/.local/bin:$PATH"

is_git_repo() {
    git rev-parse --is-inside-work-tree >/dev/null 2>&1
}

if is_git_repo; then
    exec lazygit
fi

while ! is_git_repo; do
    clear
    echo -e "\033[38;2;96;180;138m🌿 [hyprmux :: lazygit]\033[0m"
    echo -e "\033[38;2;220;220;204m현재 위치:\033[0m \033[38;2;140;208;211m$(pwd)\033[0m \033[38;2;223;175;143m(Git 저장소 아님)\033[0m"
    echo -e "\033[38;2;112;144;128m─────────────────────────────────────────────────\033[0m"

    REPOS=()
    while IFS= read -r repo; do
        if [ -n "$repo" ] && [ "$repo" != "$HOME" ] && git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
            REPOS+=("$repo")
        fi
    done < <(find "$HOME/workspace" "$HOME" -maxdepth 3 -name ".git" -type d 2>/dev/null | sed 's|/\.git$||' | sort -u | head -n 6)

    if [ ${#REPOS[@]} -gt 0 ]; then
        echo -e "\033[38;2;240;222;174m발견된 Git 프로젝트:\033[0m"
        for i in "${!REPOS[@]}"; do
            echo -e "  \033[38;2;96;180;138m[$((i+1))]\033[0m ${REPOS[$i]}"
        done
        echo -e "\033[38;2;112;144;128m─────────────────────────────────────────────────\033[0m"
        echo -e "번호 \033[38;2;96;180;138m[1-${#REPOS[@]}]\033[0m을 누르면 해당 프로젝트의 lazygit으로 시작합니다."
        echo -e "\033[38;2;140;208;211m[Enter]\033[0m를 누르면 프로젝트 이동용 쉘로 진입합니다."
        read -r -n 1 -p "선택: " choice
        echo ""

        if [[ "$choice" =~ ^[1-9]$ ]] && [ "$choice" -le "${#REPOS[@]}" ]; then
            target="${REPOS[$((choice-1))]}"
            cd "$target" || true
            exec lazygit
        else
            break
        fi
    else
        echo -e "프로젝트 디렉터리로 이동(cd) 후 lazygit을 실행하세요."
        break
    fi
done

exec bash

~/bin/hyprmux

단일 명령으로 CDP 활성화 Chromium을 실행하고, Kitty 3분할 세션을 열거나 기존 터미널을 인플레이스(In-Place) 전환한 뒤, Hyprland IPC를 호출해 두 창을 활성 작업 공간에서 탭 그룹으로 병합합니다. 실행 권한(chmod +x)을 부여합니다.

#!/usr/bin/env bash
export PATH="$HOME/bin:$HOME/.local/bin:$PATH"

if [ "$1" = "stop" ] || [ "$1" = "kill" ] || [ "$1" = "--stop" ]; then
    exec "$HOME/bin/hyprmux-stop"
fi

TARGET_DIR="$PWD"
if [ -n "$1" ] && [ -d "$1" ]; then
    TARGET_DIR=$(realpath "$1")
fi

if ! git -C "${TARGET_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    echo -e "\033[31m[hyprmux] 오류: '${TARGET_DIR}'은(는) Git 저장소가 아닙니다.\033[0m" >&2
    exit 1
fi

WORK_DIR=$(git -C "${TARGET_DIR}" rev-parse --show-toplevel)
cd "${WORK_DIR}" || exit 1

CHROME_PROFILE="$HOME/.config/chromium-hyprmux"
CDP_PORT=9222
KITTY_SOCK="/tmp/hyprmux.sock"
KITTY_SESSION="$HOME/.config/kitty/hyprmux.session"

# 1. Chromium CDP 백그라운드 구동
if ! curl -s "http://127.0.0.1:${CDP_PORT}/json/version" >/dev/null 2>&1; then
    mkdir -p "${CHROME_PROFILE}"
    nohup chromium \
        --remote-debugging-port="${CDP_PORT}" \
        --remote-allow-origins="*" \
        --user-data-dir="${CHROME_PROFILE}" \
        --no-first-run \
        --no-default-browser-check \
        --password-store=basic \
        about:blank </dev/null >/dev/null 2>&1 &

    while ! curl -s "http://127.0.0.1:${CDP_PORT}/json/version" >/dev/null 2>&1; do
        sleep 0.1
    done
fi

# 2. Kitty 터미널 환경 구성 (기존 터미널 인플레이스 전환 또는 신규 실행)
if [ -n "${KITTY_WINDOW_ID}" ] && kitty @ ls >/dev/null 2>&1; then
    kitty @ goto-layout splits 2>/dev/null || true
    kitty @ set-window-title --match="id:${KITTY_WINDOW_ID}" "AGENT" 2>/dev/null || true
    CURRENT_WINDOWS=$(kitty @ ls 2>/dev/null | jq -r '.[0].tabs[0].windows[] | .title')

    if ! echo "${CURRENT_WINDOWS}" | grep -q "^LAZYGIT$"; then
        kitty @ launch --location=vsplit --bias=40 --title="LAZYGIT" --cwd="${WORK_DIR}" lazygit 2>/dev/null || true
    fi
    if ! echo "${CURRENT_WINDOWS}" | grep -q "^YAZI$"; then
        kitty @ launch --location=hsplit --bias=50 --title="YAZI" --cwd="${WORK_DIR}" yazi 2>/dev/null || true
    fi
    kitty @ focus-window --match="id:${KITTY_WINDOW_ID}" 2>/dev/null || true
elif ! kitty @ --to="unix:${KITTY_SOCK}" ls >/dev/null 2>&1; then
    nohup kitty --directory="${WORK_DIR}" --listen-on="unix:${KITTY_SOCK}" --session="${KITTY_SESSION}" </dev/null >/dev/null 2>&1 &
fi

# 3. Hyprland 탭 그룹화 동기화
python3 -c '
import json, subprocess, time, sys, os

try:
    ws_raw = subprocess.check_output(["hyprctl", "activeworkspace", "-j"], stderr=subprocess.DEVNULL).decode("utf-8")
    target_ws = json.loads(ws_raw).get("id", 1)
except Exception:
    target_ws = 1

def is_hyprmux_chrome(client):
    if "chromium" not in client.get("class", "").lower():
        return False
    pid = client.get("pid")
    if not pid:
        return False
    try:
        with open(f"/proc/{pid}/cmdline", "rb") as f:
            return "chromium-hyprmux" in f.read().decode("utf-8", errors="ignore")
    except Exception:
        return False

for _ in range(35):
    try:
        clients = json.loads(subprocess.check_output(["hyprctl", "clients", "-j"], stderr=subprocess.DEVNULL).decode("utf-8"))
    except Exception:
        clients = []

    ws_kitties = [c for c in clients if c.get("class") == "kitty" and c.get("workspace", {}).get("id") == target_ws]
    ws_chromes = [c for c in clients if is_hyprmux_chrome(c) and c.get("workspace", {}).get("id") == target_ws]

    if ws_kitties and ws_chromes:
        k_win, c_win = ws_kitties[0], ws_chromes[0]
        k_addr, c_addr = k_win["address"], c_win["address"]
        if c_addr in k_win.get("grouped", []):
            sys.exit(0)

        if not k_win.get("grouped", []):
            subprocess.run(["hyprctl", "dispatch", f"hl.dsp.focus({{ window = \"address:{k_addr}\" }})"], stdout=subprocess.DEVNULL)
            subprocess.run(["hyprctl", "dispatch", "hl.dsp.group.toggle()"], stdout=subprocess.DEVNULL)
            time.sleep(0.15)

        subprocess.run(["hyprctl", "dispatch", f"hl.dsp.focus({{ window = \"address:{c_addr}\" }})"], stdout=subprocess.DEVNULL)
        time.sleep(0.15)
        direction = "left" if c_win.get("at", [0, 0])[0] >= k_win.get("at", [0, 0])[0] else "right"
        subprocess.run(["hyprctl", "dispatch", f"hl.dsp.window.move({{ into_group = \"{direction}\" }})"], stdout=subprocess.DEVNULL)
        time.sleep(0.15)
        subprocess.run(["hyprctl", "dispatch", f"hl.dsp.focus({{ window = \"address:{k_addr}\" }})"], stdout=subprocess.DEVNULL)
        sys.exit(0)
    time.sleep(0.1)
' || true

~/bin/hyprmux-stop

세션을 생성 역순으로 안전하게 종료하고, 인플레이스 전환된 터미널을 원래 상태로 복원합니다. 개인 브라우저는 건드리지 않은 채 hyprmux 전용 프로세스와 포트만 정리합니다. 실행 권한(chmod +x)을 부여합니다.

#!/usr/bin/env bash
export PATH="$HOME/bin:$HOME/.local/bin:$PATH"

# 중복/경합 실행 방지 (flock 뮤텍스 락)
LOCK_FILE="/tmp/hyprmux-stop.lock"
exec 200>"${LOCK_FILE}"
if ! flock -n 200; then
    exit 0
fi

CDP_PORT=9222
ACTIVE_WS=$(hyprctl activeworkspace -j 2>/dev/null | jq -r '.id // 1')
STATE_FILE="/tmp/hyprmux_ws${ACTIVE_WS}.state"
[ ! -f "${STATE_FILE}" ] && STATE_FILE="/tmp/hyprmux.state"

MODE="inplace"
ORIG_WINDOW_ID=""
ORIG_TITLE=""
ORIG_LAYOUT=""
KITTY_SOCK=""
KITTY_PID=""

HAD_STATE=false
if [ -f "${STATE_FILE}" ]; then
    HAD_STATE=true
    source "${STATE_FILE}"
fi

CHANGED=false

# 1. Hyprland 탭 그룹 해제
UNGROUPED=$(python3 -c '
import json, subprocess, time

try:
    ws_raw = subprocess.check_output(["hyprctl", "activeworkspace", "-j"], stderr=subprocess.DEVNULL).decode("utf-8")
    active_ws = json.loads(ws_raw).get("id", 1)
except Exception:
    active_ws = 1

try:
    clients = json.loads(subprocess.check_output(["hyprctl", "clients", "-j"], stderr=subprocess.DEVNULL).decode("utf-8"))
except Exception:
    clients = []

ws_kitties = [c for c in clients if c.get("class") == "kitty" and c.get("workspace", {}).get("id") == active_ws]
if not ws_kitties:
    ws_kitties = [c for c in clients if c.get("class") == "kitty"]

ungrouped = False
for k in ws_kitties:
    if k.get("grouped"):
        k_addr = k.get("address")
        subprocess.run(["hyprctl", "dispatch", f"hl.dsp.focus({{ window = \"address:{k_addr}\" }})"], stdout=subprocess.DEVNULL)
        subprocess.run(["hyprctl", "dispatch", "hl.dsp.group.toggle()"], stdout=subprocess.DEVNULL)
        ungrouped = True
        time.sleep(0.08)

if ungrouped:
    print("yes")
' 2>/dev/null || true)

[ "${UNGROUPED}" = "yes" ] && CHANGED=true

# 2. hyprmux 전용 Chromium 및 CDP 포트 정리
CHROME_PIDS=()
for pid in $(pgrep -f "chromium" 2>/dev/null); do
    if [ -f "/proc/${pid}/cmdline" ]; then
        cmd=$(tr '\0' ' ' < "/proc/${pid}/cmdline" 2>/dev/null || true)
        if echo "${cmd}" | grep -q "chromium-hyprmux"; then
            CHROME_PIDS+=("${pid}")
        fi
    fi
done

if [ ${#CHROME_PIDS[@]} -gt 0 ]; then
    kill -TERM "${CHROME_PIDS[@]}" 2>/dev/null || true
    sleep 0.15
    for pid in "${CHROME_PIDS[@]}"; do
        if kill -0 "${pid}" 2>/dev/null; then
            kill -KILL "${pid}" 2>/dev/null || true
        fi
    done
    CHANGED=true
fi

if command -v systemctl >/dev/null 2>&1; then
    if systemctl --user is-active chromium-hyprmux >/dev/null 2>&1; then
        systemctl --user stop chromium-hyprmux >/dev/null 2>&1 || true
        CHANGED=true
    fi
    systemctl --user reset-failed chromium-hyprmux >/dev/null 2>&1 || true
fi

if command -v fuser >/dev/null 2>&1; then
    if fuser "${CDP_PORT}/tcp" >/dev/null 2>&1; then
        fuser -k "${CDP_PORT}/tcp" >/dev/null 2>&1 || true
        CHANGED=true
    fi
fi

# 3. Kitty 소켓 탐색 및 역순 창 정리 (Yazi -> Lazygit -> 원래 터미널 복구)
find_kitty_sockets() {
    local sockets=()
    if [ -n "${KITTY_SOCK}" ] && [ -S "${KITTY_SOCK#unix:}" ]; then
        sockets+=("${KITTY_SOCK}")
    fi
    local client_pids
    client_pids=$(hyprctl clients -j 2>/dev/null | jq -r --argjson ws "${ACTIVE_WS}" '.[] | select(.class == "kitty" and .workspace.id == $ws) | .pid' 2>/dev/null || true)
    for p in ${client_pids}; do
        [ -S "/tmp/hyprmux.sock-${p}" ] && sockets+=("unix:/tmp/hyprmux.sock-${p}")
    done
    [ -S "/tmp/hyprmux.sock" ] && sockets+=("unix:/tmp/hyprmux.sock")
    for s in /tmp/hyprmux.sock-*; do
        [ -S "${s}" ] && sockets+=("unix:${s}")
    done
    printf '%s\n' "${sockets[@]}" | sort -u
}

while IFS= read -r sock; do
    [ -z "${sock}" ] && continue
    ALL_TITLES=$(kitty @ --to="${sock}" ls 2>/dev/null | jq -r '.[].tabs[].windows[] | .title' 2>/dev/null || true)
    [ -z "${ALL_TITLES}" ] && continue

    if echo "${ALL_TITLES}" | grep -qE '^(YAZI|LAZYGIT|AGENT)$'; then
        # 역순 1: Yazi 패널 닫기 (가장 마지막에 생성됨)
        if echo "${ALL_TITLES}" | grep -q "^YAZI$"; then
            kitty @ --to="${sock}" close-window --match="title:^YAZI$" --ignore-no-match 2>/dev/null || true
            CHANGED=true
            sleep 0.05
        fi

        # 역순 2: Lazygit 패널 닫기
        if echo "${ALL_TITLES}" | grep -q "^LAZYGIT$"; then
            kitty @ --to="${sock}" close-window --match="title:^LAZYGIT$" --ignore-no-match 2>/dev/null || true
            CHANGED=true
            sleep 0.05
        fi

        # 역순 3: 원래 터미널 상태로 복원
        IS_STANDALONE=false
        [ "${MODE}" = "standalone" ] && IS_STANDALONE=true
        KPID=$(basename "${sock}" | sed 's/hyprmux\.sock-//')
        if [ -f "/proc/${KPID}/cmdline" ] && grep -q "hyprmux\.session" "/proc/${KPID}/cmdline" 2>/dev/null; then
            IS_STANDALONE=true
        fi

        if [ "${IS_STANDALONE}" = true ]; then
            kitty @ --to="${sock}" close-window --match="title:^AGENT$" --ignore-no-match 2>/dev/null || true
            kitty @ --to="${sock}" quit 2>/dev/null || true
            systemctl --user stop kitty-hyprmux >/dev/null 2>&1 || true
            rm -f "${sock#unix:}" 2>/dev/null || true
            CHANGED=true
        else
            if echo "${ALL_TITLES}" | grep -q "^AGENT$"; then
                if [ -n "${ORIG_WINDOW_ID}" ]; then
                    kitty @ --to="${sock}" focus-window --match="id:${ORIG_WINDOW_ID}" 2>/dev/null || true
                else
                    kitty @ --to="${sock}" focus-window --match="title:^AGENT$" 2>/dev/null || true
                fi

                if [ -n "${ORIG_TITLE}" ] && [ "${ORIG_TITLE}" != "AGENT" ]; then
                    kitty @ --to="${sock}" set-window-title --match="title:^AGENT$" "${ORIG_TITLE}" 2>/dev/null || true
                else
                    kitty @ --to="${sock}" set-window-title --match="title:^AGENT$" "" 2>/dev/null || true
                fi

                if [ -n "${ORIG_LAYOUT}" ] && [ "${ORIG_LAYOUT}" != "splits" ]; then
                    kitty @ --to="${sock}" goto-layout "${ORIG_LAYOUT}" 2>/dev/null || true
                fi
                CHANGED=true
            fi
        fi
    fi
done < <(find_kitty_sockets)

if [ "${HAD_STATE}" = true ] || [ -f "/tmp/hyprmux_ws${ACTIVE_WS}.state" ] || [ -f "/tmp/hyprmux.state" ]; then
    rm -f "/tmp/hyprmux_ws${ACTIVE_WS}.state" "/tmp/hyprmux.state" 2>/dev/null || true
    CHANGED=true
fi

if [ "${CHANGED}" = true ]; then
    if command -v hyprctl >/dev/null 2>&1; then
        hyprctl notify 1 2000 "rgb(93e0e3)" "hyprmux: Reverted to Original Terminal" >/dev/null 2>&1 || true
    fi
fi
~/.config/hypr/hyprland.conf

Hyprland에서 탭 그룹 창을 손쉽게 전환할 수 있도록 키바인딩을 지정합니다.

# 그룹 내 다음 탭으로 순환
bind = $mainMod, Tab, changegroupactive, f
# 그룹 내 이전 탭으로 순환
bind = $mainMod SHIFT, Tab, changegroupactive, b

# 탭 그룹 토글
bind = $mainMod, G, togglegroup

3. 서비스 실행 및 확인

3.1 세션 기동 방식

hyprmux는 실행 위치와 현재 터미널 상태에 따라 자동으로 최적의 모드로 진입합니다.

cd ~/workspace/my-project
hyprmux
  1. 신규 실행 모드: 터미널 외부 또는 새 작업 공간에서 실행 시, ~/.config/kitty/hyprmux.session을 읽어 Kitty 3분할 윈도우를 띄우고 Chromium CDP 인스턴스를 실행하여 활성 작업 공간에서 자동으로 탭 그룹으로 결합합니다.
  2. 인플레이스 전환(In-Place Transformation) 모드: 이미 작업 중이던 Kitty 터미널 안에서 hyprmux를 입력하면, 새로운 창을 띄우지 않고 현재 윈도우를 좌측 60% AGENT로 재설정하며 우측에 LAZYGIT과 YAZI 패널을 즉각 분할 생성합니다.

3.2 일상 개발 워크플로우

hyprmux 환경이 활성화되면 세 개의 터미널 영역과 브라우저가 다음과 같이 상호보완적으로 동작합니다.

  1. 에이전트 작업 (AGENT): 좌측 메인 쉘에서 agy(Antigravity CLI)를 실행하여 코드 작성, 리팩터링, 테스트 명령을 지시합니다.
  2. 파일 탐색 및 검토 (YAZI): 에이전트가 새로 생성한 파일이나 이미지 에셋을 우측 하단 Yazi에서 방향키로 빠르게 이동하며 미리보기합니다.
  3. 변경점 추적 및 커밋 (LAZYGIT): 에이전트가 파일들을 수정한 즉시 우측 상단 Lazygit에 변경 내역(diff)이 실시간으로 갱신됩니다. 에이전트 작업 완료 후 Space로 스테이징하고 c로 즉시 체크포인트 커밋을 남깁니다.
  4. 브라우저 화면 검증 (Chromium): $mainMod + Tab을 누르면 Kitty 3분할 창에서 Chromium 브라우저로 즉각 화면이 전환되어 개발 중인 웹 UI를 검증할 수 있습니다.

3.3 AI 에이전트의 브라우저 제어 (CDP 연동)

Chromium이 열어둔 9222 포트는 에이전트가 브라우저를 프로그래밍 방식으로 조작할 수 있는 원격 엔드포인트입니다.

엔드포인트 응답 상태 확인:

curl -s http://127.0.0.1:9222/json/version

정상 응답 예시:

{
   "Browser": "Chrome/133.0.0.0",
   "Protocol-Version": "1.3",
   "User-Agent": "Mozilla/5.0 ...",
   "V8-Version": "...",
   "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/browser/..."
}

Python 스크립트나 AI 에이전트 툴체인(Playwright)에서 실행 중인 브라우저에 연결하는 예시:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp("http://127.0.0.1:9222")
    context = browser.contexts[0]
    page = context.new_page()
    page.goto("http://localhost:3000")
    page.screenshot(path="screenshot.png")

3.4 주요 단축키 및 환경 조작

hyprmux 환경은 Hyprland 윈도우 매니저, Kitty 터미널 멀티플렉서, TUI 생산성 도구의 3단계 계층으로 제어됩니다.

1) Hyprland 창 관리 및 hyprmux 탭 그룹 ($mainMod = SUPER)

단축키 명령/동작 기능 설명
$mainMod + Tab hl.dsp.group.next() Kitty 3분할 터미널 ↔ Chromium 브라우저 탭 순환 전환
$mainMod + Shift + Tab hl.dsp.group.prev() 탭 그룹 내 이전 창으로 역방향 순환
$mainMod + G hl.dsp.group.toggle() 활성 창 탭 그룹 결합 또는 그룹 해제
$mainMod + Shift + G hl.dsp.window.move({ into_group = "left" }) 현재 창을 왼쪽 탭 그룹으로 강제 편입
$mainMod + Shift + D exec, hyprmux-stop hyprmux 역순 종료 및 원래 터미널 복귀 (Yazi/Lazygit 역순 닫기, 터미널 복원, 탭 그룹 해제, Chromium CDP 종료)
$mainMod + H / J / K / L hl.dsp.focus({ direction = ... }) Vim 스타일 활성 윈도우 포커스 이동 (좌 / 하 / 상 / 우)
$mainMod + F hl.dsp.window.fullscreen() 활성 창 전체 화면 토글
$mainMod + Space hl.dsp.window.float() 타일링 / 플로팅 윈도우 전환 토글
$mainMod + Q hl.dsp.window.kill() 활성 윈도우 닫기
$mainMod + 1 ~ 3 hl.dsp.focus({ workspace = ... }) 가상 작업 공간(Workspace) 1~3 바로 이동

2) Kitty 터미널 3분할 멀티플렉싱 (Ctrl + Shift)

단축키 기능 설명
Ctrl + Shift + [ 이전 분할 패널로 포커스 이동 (AGENT ← LAZYGIT ← YAZI)
Ctrl + Shift + ] 다음 분할 패널로 포커스 이동 (AGENT → LAZYGIT → YAZI)
Ctrl + Shift + Z 스택 줌(Stack/Zoom) 토글: 현재 패널을 터미널 전체로 확대 / 복귀 (에이전트 코드 집중)
Ctrl + Shift + R 분할 패널 크기 조절(Resize) 모드 진입 (방향키로 조절 후 Esc로 완료)
Ctrl + Shift + L 레이아웃 순환 (splits ↔ stack ↔ tall ↔ fat ↔ grid)
Ctrl + Shift + Enter 현재 레이아웃에서 새 분할 쉘 추가
Ctrl + Shift + W 현재 분할 패널 닫기
Ctrl + Shift + C / V 터미널 텍스트 복사 / 붙여넣기
Ctrl + Shift + Page_Up / Down 터미널 출력 버퍼 이전 / 다음 페이지 스크롤

3) TUI 도구 (Lazygit & Yazi) 핵심 조작키

도구 단축키 기능 설명
Lazygit
(우측 상단 20%)
Space 파일 스테이징 / 언스테이징 토글
c Git 커밋 메시지 작성
P / p 원격 저장소 푸시(Push) / 풀(Pull)
d 변경 사항 폐기(Discard / Reset)
z 직전 Git 작업 실행 취소(Undo)
1 ~ 5 메인 패널 전환 (1: 상태, 2: 파일, 3: 브랜치, 4: 커밋, 5: 스태시)
q Lazygit 종료 (Git 저장소 미감지 시 스마트 런처로 복귀)
Yazi
(우측 하단 20%)
h / j / k / l 디렉터리 상위 이동 / 아래 / 위 / 진입 또는 파일 열기 (방향키 호환)
Space 파일 다중 선택 토글
y / x / p 파일 복사(Yank) / 잘라내기(Cut) / 붙여넣기(Paste)
d 파일 삭제 (시스템 휴지통 이동)
. (마침표) 숨김 파일(Hidden files) 표시 토글
q Yazi 종료

3.5 세션 종료 및 원래 터미널 복귀

개발 작업을 마치면 단일 명령(hyprmux stop 또는 $mainMod + Shift + D)으로 hyprmux 관련 프로세스 및 분할 패널을 생성 역순으로 안전하게 정리합니다.

hyprmux stop
  1. Hyprland 탭 그룹 해제: Kitty와 Chromium의 탭 그룹을 해제하여 Kitty를 본래의 독립 타일링 윈도우로 분리합니다.
  2. Chromium 및 디버깅 포트 종료: 개인 브라우저는 보존한 채 전용 Chromium(~/.config/chromium-hyprmux)과 9222 CDP 포트만 안전하게 종료합니다.
  3. Kitty 분할 패널 역순 닫기: 가장 마지막에 생성된 YAZI 패널을 먼저 닫고, 이어서 LAZYGIT 패널을 닫습니다.
  4. 원래 터미널 복원: 인플레이스 전환 모드였던 경우 기존 메인 쉘(AGENT) 창을 닫지 않고 타이틀과 레이아웃을 본래 상태로 복원하며 포커스를 맞춥니다. (신규 독립 세션 모드였던 경우 전체 세션을 깔끔하게 종료합니다.)

4. 트러블슈팅

Warning

Chromium을 실행할 때 --user-data-dir를 분리하지 않으면 이미 실행 중인 개인 Chromium 브라우저 프로세스에 탭만 추가되어 --remote-debugging-port 옵션이 무시됩니다. 반드시 ~/.config/chromium-hyprmux와 같이 독립된 프로필 디렉터리를 지정해야 에이전트 자동화용 CDP 포트가 열립니다.

Warning

포트 9222가 이미 다른 디버거 프로세스에 의해 점유되어 있거나 비정상 종료 후 남아있는 경우 curl -s http://127.0.0.1:9222/json/version 응답이 실패할 수 있습니다. fuser -k 9222/tcp로 포트를 회수한 뒤 hyprmux를 재실행합니다.

Warning

기존 실행 중인 Kitty 터미널 안에서 hyprmux를 실행할 때 인플레이스 창 분할이 동작하려면 ~/.config/kitty/kitty.conf에 allow_remote_control yes 또는 allow_remote_control socket-only 설정이 켜져 있어야 합니다.