-- =========================================================
--  WezTerm Configuration  (v2 - 검증본)
--  Windows / macOS / Linux 공통  |  한국어 + 영어 최적화
--
--  설치 위치 (세 OS 모두 홈 디렉토리):
--    Windows : %USERPROFILE%\.wezterm.lua
--    macOS   : /Users/<사용자명>/.wezterm.lua
--    Linux   : /home/<사용자명>/.wezterm.lua
--
--  검증 기준: wezterm.org 공식 문서 (config/default-keys, fonts,
--             keys, window_decorations, files)
-- =========================================================

local wezterm = require 'wezterm'
local act = wezterm.action
local config = wezterm.config_builder()

-- ---------------------------------------------------------
-- 1. 플랫폼 판별
-- ---------------------------------------------------------
local triple = wezterm.target_triple
local is_windows = triple:find('windows') ~= nil
local is_mac = triple:find('darwin') ~= nil
local is_linux = triple:find('linux') ~= nil

-- 파일 존재 확인.
-- [주의] wezterm.run_child_process 는 프로그램이 없을 때 false 를 반환하지 않고
--        Lua 에러를 던진다("No such file or directory"). 설정 최상위에서
--        이를 쓰면 창 대신 Configuration Error 가 뜬다. 따라서 사용하지 않는다.
--        io.open 은 실패 시 nil 을 돌려주므로 안전하고 프로세스도 띄우지 않는다.
local function file_exists(path)
  local ok, f = pcall(io.open, path, 'r')
  if ok and f then f:close(); return true end
  return false
end

-- WezTerm 빌드 날짜 비교 (예: '20240203-110809-...' -> 20240203)
-- 신버전 전용 옵션을 구버전에서 켜면 설정 전체가 로드 실패하므로 게이트가 필요하다.
local BUILD = tonumber(tostring(wezterm.version):match('^(%d%d%d%d%d%d%d%d)') or '0') or 0
local function build_at_least(d) return BUILD >= d end

-- 여러 후보 경로 중 처음 존재하는 것을 반환 (없으면 nil)
local function first_existing(paths)
  for _, p in ipairs(paths) do
    if file_exists(p) then return p end
  end
  return nil
end

-- ---------------------------------------------------------
-- 2. 폰트 : 영문 주폰트 + 한글 폴백
--    WezTerm은 지정한 목록 뒤에 Nerd Font Symbols 등
--    자체 기본 폴백을 자동 추가하므로 따로 나열할 필요 없음
-- ---------------------------------------------------------
local cjk_font
if is_windows then
  cjk_font = 'Malgun Gothic'        -- 윈도우 기본 탑재
elseif is_mac then
  cjk_font = 'Apple SD Gothic Neo'  -- macOS 기본 탑재
else
  cjk_font = 'Noto Sans Mono CJK KR'
end

-- [정정] 이전 버전에서 CJK 폰트에 scale=1.33 을 넣었으나 이는 잘못이었다.
--   한글 x_adv 가 셀폭보다 작은 것은 결함이 아니라 폰트의 의도된 좌우 여백이다.
--   1.33 배로 강제 확대하면 글리프가 픽셀 격자에서 벗어나 힌팅이 깨지고
--   화면이 뿌옇게 흐려진다. 크기 조절은 scale 이 아니라 font_size 로 해야 한다.
config.font = wezterm.font_with_fallback {
  -- 'JetBrains Mono' 는 WezTerm 내장 폰트라 설치가 필요 없고,
  -- Nerd Font 아이콘은 내장 'Symbols Nerd Font Mono' 가 자동 폴백된다.
  { family = 'JetBrains Mono' },
  { family = 'D2Coding' },   -- 한글 고정폭. 설치돼 있으면 우선 사용
  { family = cjk_font },     -- OS 기본 한글 폰트
  'Noto Color Emoji',
}

-- 크기: 기본 12 에서 한 단계 낮춘 11 을 기본값으로 둔다.
-- 크게 쓰고 싶으면 이 값만 12~13 으로 올릴 것 (scale 은 건드리지 말 것)
config.font_size = is_mac and 13.0 or 11.0

-- [중요] 탭바·명령팔레트 같은 UI 요소는 config.font 가 아니라 각자 폰트를 쓴다.
--   단일 폰트만 지정하면 한글 탭 이름이 두부(□)로 렌더된다.
--   UI 전용 폴백 체인을 만들어 모든 UI 폰트에 공통으로 넘긴다.
local function ui_font(weight)
  return wezterm.font_with_fallback {
    { family = 'JetBrains Mono', weight = weight or 'Regular' },
    { family = 'D2Coding' },
    { family = cjk_font },
    'Noto Color Emoji',
  }
end
config.line_height = 1.1
config.cell_width = 1.0

-- CJK 폰트는 cap-height 지표가 부정확하므로 자동 스케일링을 끈다
config.use_cap_height_to_scale_fallback_fonts = false
config.warn_about_missing_glyphs = false

-- 글자 선명도.
--   Windows/Linux LCD 모니터에서는 서브픽셀 렌더링(HorizontalLcd)이 가장 또렷하다.
--   색 번짐(빨강/파랑 테두리)이 보이면 'Normal' 로 바꿀 것.
--   macOS 는 Retina 라 Normal 이 자연스럽다.
if is_mac then
  config.freetype_load_target = 'Normal'
  config.freetype_render_target = 'Normal'
else
  config.freetype_load_target = 'Light'
  config.freetype_render_target = 'HorizontalLcd'
end

config.adjust_window_size_when_changing_font_size = false

-- ---------------------------------------------------------
-- 3. 한국어 처리 (★ 핵심)
-- ---------------------------------------------------------
-- 한글 입력기(IME) 활성화
config.use_ime = true

-- macOS 는 한글 파일명을 NFD(자모 분리)로 저장한다.
-- 이 옵션이 없으면 ls 결과가 "ㅎㅏㄴㄱㅡㄹ" 처럼 분리되어 보인다.
config.normalize_output_to_unicode_nfc = true

-- 동아시아 애매폭 문자(─ │ ┌ ● ★ 등)를 2칸으로 처리할지 여부.
-- tree / htop / 박스 그리기가 어긋나면 true 로 바꿔 볼 것.
-- 반대로 이미 잘 나오는데 true 로 두면 오히려 어긋나므로 기본은 false.
config.treat_east_asian_ambiguous_width_as_wide = false

-- ---------------------------------------------------------
-- 4. 외관
-- ---------------------------------------------------------
-- =========================================================
--  밝은 테마 (눈에 편한 따뜻한 오프화이트)
--
--  [배경 선택 근거]
--    순백(#ffffff, 휘도 255)은 눈부심이 크다. 여기서는 휘도 238의
--    따뜻한 미색을 쓴다. 밝지만 눈이 덜 피로하다.
--
--  [ANSI 색을 직접 정의한 이유]
--    내장 밝은 테마 20종을 전수 측정한 결과 ANSI 색 최저 대비가
--    최고 2.11, 대부분 1.0~1.6 으로 WCAG AA(4.5)에 한참 못 미쳤다.
--    (밝은 배경 위에 밝은 색을 올리니 구조적으로 안 나온다)
--    그래서 Gruvbox Light 색상을 기준으로 색조·채도는 유지한 채
--    명도만 낮춰 모든 색이 4.5 이상이 되도록 재계산했다.
--    bright 계열은 7.0 이상으로 맞춰 굵은 글씨가 8/8 구분된다.
-- =========================================================
local P = {
  bg        = '#f6eeda',   -- 배경 (휘도 238)
  fg        = '#3c3836',   -- 전경 (대비 10.03)
  accent    = '#1d69ce',   -- 파랑 강조 (4.58)
  warn      = '#8e6516',   -- 주황 강조 (4.51)
  tab_bar   = '#e7e0cd',   -- 탭바 배경
  tab_ina   = '#6d6258',   -- 비활성 탭 글자 (4.51)
  popup_bg  = '#fbf7ee',   -- 팝업 배경 (글자 대비 10.84)
  thumb     = '#b1ab9d',   -- 스크롤바
  sel_bg    = '#c6d1d7',   -- 선택 영역 (글자 대비 7.46)
  dim       = '#5a5048',   -- 보조 텍스트 (탭바 위 대비 5.96)
  chip      = '#b5a98c',   -- 상태줄 칩 (탭바와 구분 1.77 / 글자 대비 4.98)
}

config.color_schemes = {
  ['Warm Light AA'] = {
    background = P.bg,
    foreground = P.fg,
    cursor_bg = P.accent,
    cursor_fg = P.bg,
    cursor_border = P.accent,
    selection_bg = P.sel_bg,
    selection_fg = P.fg,

    --        검정      빨강      초록      노랑      파랑      자홍      청록      흰색
    ansi    = { '#282828','#cc241d','#707013','#8e6516','#3d7578','#9c5676','#4e7650','#5f5750' },
    brights = { '#6d6258','#9b1b16','#52520e','#684a10','#2d5558','#723f56','#39563a','#3c3836' },
  },
}
config.color_scheme = 'Warm Light AA'

-- 투명도는 밝은 테마에서 특히 대비를 크게 깎는다. 불투명 유지.
config.window_background_opacity = 1.0

-- 세 OS 모두 TITLE|RESIZE 로 통일한다.
--   RESIZE 단독            -> 타이틀바 없음. 탭 1개면 탭바도 숨겨져 창 이동 불가
--   INTEGRATED_BUTTONS 단독 -> 창 버튼이 탭바에 들어가므로 위와 같은 문제 발생
config.window_decorations = 'TITLE|RESIZE'

-- [렌더링] 화면 찢김(가로 흰줄)과 랙의 주원인은 GPU 어댑터 선택 실패다.
--   노트북처럼 GPU 가 여러 개인 환경에서 WezTerm 이 소프트웨어 렌더러나
--   엉뚱한 어댑터를 잡으면 프레임이 밀리고 가로줄이 생긴다.
--   우선순위: Dx12/Metal > Vulkan > OpenGL, 외장 > 내장 (Cpu 는 제외)
local function pick_gpu()
  local ok, gpus = pcall(function() return wezterm.gui.enumerate_gpus() end)
  if not ok or type(gpus) ~= 'table' then return nil end
  local backend_rank = { Dx12 = 40, Metal = 40, Vulkan = 30, Gl = 10 }
  local device_rank  = { DiscreteGpu = 3, IntegratedGpu = 2, Other = 1, Cpu = 0 }
  local best, best_score = nil, 0
  for _, g in ipairs(gpus) do
    local d = device_rank[tostring(g.device_type)] or 0
    if d > 0 then
      local score = (backend_rank[tostring(g.backend)] or 0) + d
      if score > best_score then best, best_score = g, score end
    end
  end
  return best
end

local gpu = pick_gpu()
if gpu then
  config.front_end = 'WebGpu'
  config.webgpu_preferred_adapter = gpu
  config.webgpu_power_preference = 'HighPerformance'
else
  config.front_end = 'OpenGL'
end

config.max_fps = 120
config.enable_kitty_keyboard = false

if is_mac then
  config.macos_window_background_blur = 0
end

config.window_padding = { left = 12, right = 12, top = 10, bottom = 8 }
config.window_close_confirmation = 'AlwaysPrompt'
-- 스크롤백은 패널마다 따로 잡힌다. 분할을 자주 하면 그만큼 메모리와
-- 초기화 비용이 늘어난다. 10000 줄이면 실사용에 충분하다.
config.scrollback_lines = 10000
config.enable_scroll_bar = true
config.default_cursor_style = 'SteadyBar'
config.cursor_blink_rate = 0
config.cursor_blink_ease_in = 'Constant'
config.cursor_blink_ease_out = 'Constant'
config.audible_bell = 'Disabled'

-- 밝은 테마에서는 밝기를 낮추면 오히려 대비가 살짝 올라간다(10.03 -> 8.95).
-- 채도만 조금 빼서 비활성 패널을 '바래 보이게' 하는 편이 자연스럽다.
config.inactive_pane_hsb = { saturation = 0.75, brightness = 0.97 }
config.hide_mouse_cursor_when_typing = true

-- ---- 사용 편의 ----
-- 탭바 위에서 마우스 휠 -> 탭 전환
config.mouse_wheel_scrolls_tabs = true

-- 다른 창에서 넘어오며 클릭할 때, 그 클릭이 터미널로 전달되지 않게 한다
config.swallow_mouse_click_on_window_focus = true

-- 셸만 돌고 있으면 탭/창을 닫을 때 확인창을 띄우지 않는다.
-- 실행 중인 작업(vim, ssh 등)이 있을 때만 물어본다.
config.skip_close_confirmation_for_processes_named = {
  'bash', 'sh', 'zsh', 'fish', 'tmux',
  'cmd.exe', 'pwsh.exe', 'powershell.exe',
}

-- 비밀번호 입력 중이면 커서 모양으로 알려준다
config.detect_password_input = true

-- 키를 누르면 스크롤 위치를 맨 아래로 되돌린다
config.scroll_to_bottom_on_input = true

-- 파일을 창에 끌어다 놓으면 경로에 따옴표를 씌워준다 (공백/한글 경로 대응)
config.quote_dropped_files = is_windows and 'Windows' or 'Posix'

-- 팝업 UI 도 테마에 맞춘다. 지정 안 하면 밝은 회색 창이 튀어나온다.
config.command_palette_bg_color = P.popup_bg
config.command_palette_fg_color = P.fg
config.command_palette_font_size = is_mac and 13.0 or 11.0
config.command_palette_rows = 14
config.char_select_bg_color = P.popup_bg
config.char_select_fg_color = P.fg
config.char_select_font_size = is_mac and 13.0 or 11.0

-- [주의] *_font_size 는 구버전에도 있지만 *_font 는 신버전 전용이다.
--   구버전(20240203)에 지정하면 설정 전체가 로드 실패한다 — 실측으로 확인했다.
--   지원하는 빌드에서만 한글 폴백 폰트를 넘긴다.
if build_at_least(20240301) then
  config.command_palette_font = ui_font()
  config.char_select_font = ui_font()
  config.pane_select_font = ui_font('Bold')
end

-- 패널 전환 시 확대 상태 자동 해제 (확대된 채 이동하면 혼란)
config.unzoom_on_switch_pane = true

-- 상태 표시줄 갱신 주기(ms)
config.status_update_interval = 1000

-- 패키지 매니저로 설치했다면 앱 내부 업데이트 알림은 방해다
config.check_for_updates = false
config.show_update_window = false

-- 탭바
-- [중요] 탭별 닫기(x) 버튼은 fancy 탭바에서만 그려진다.
--   use_fancy_tab_bar = false(레트로)로 두면 닫기 버튼이 아예 없다.
--   show_close_tab_button_in_tabs 는 기본값이 true 라 따로 켤 필요는 없다.
config.use_fancy_tab_bar = true
config.tab_bar_at_bottom = false               -- 탭바를 창 위쪽에
config.hide_tab_bar_if_only_one_tab = false    -- 탭 1개여도 계속 표시
-- 탭 제목이 폭을 다 먹으면 오른쪽 닫기(x) 버튼이 잘려 안 보인다.
-- 제목은 짧게 자르고 탭 폭은 넉넉히 준다.
config.tab_max_width = 36

-- 닫기 버튼은 기본값이 true 지만, 구버전에는 이 옵션 자체가 없어
-- 그냥 쓰면 설정 로드가 통째로 실패한다. 지원하는 빌드에서만 명시한다.
if build_at_least(20240301) then
  config.show_close_tab_button_in_tabs = true
end
config.show_new_tab_button_in_tab_bar = true   -- + 버튼 (동작은 아래에서 분할로 바꿈)
config.show_tab_index_in_tab_bar = true
config.switch_to_last_active_tab_when_closing_tab = true  -- 탭 닫으면 직전 탭으로

-- fancy 탭바는 탭 '바깥' 영역을 window_frame 으로 그린다.
-- 여기를 안 맞추면 탭바만 회색으로 떠 보인다.
config.window_frame = {
  font = ui_font('Bold'),   -- 한글 탭 이름 대응
  font_size = is_mac and 12.0 or 10.0,
  active_titlebar_bg = P.tab_bar,
  inactive_titlebar_bg = P.tab_bar,
  active_titlebar_fg = P.fg,
  inactive_titlebar_fg = P.dim,
  button_bg = P.tab_bar,
  button_fg = P.fg,
  button_hover_bg = P.accent,
  button_hover_fg = P.bg,
}

-- use_fancy_tab_bar = false 일 때 탭바는 테마 색을 따라가지 않으므로
-- ayu 팔레트에 맞춰 직접 지정한다 (지정하지 않으면 회색 기본값과 충돌)
config.colors = {
  -- 스크롤바 손잡이 (배경 대비 1.98 — 은은하게 보이는 정도)
  scrollbar_thumb = P.thumb,

  -- 패널 분할선. 지정 안 하면 밝은 배경에 묻혀 화면이 나뉜 게 안 보인다.
  split = P.accent,

  -- 한글 IME 조합 중 커서 색 (조합 중/확정 구분)
  compose_cursor = P.warn,

  -- 복사 모드 선택 강조
  copy_mode_active_highlight_bg = { Color = P.accent },
  copy_mode_active_highlight_fg = { Color = P.bg },
  copy_mode_inactive_highlight_bg = { Color = P.sel_bg },
  copy_mode_inactive_highlight_fg = { Color = P.fg },

  -- 퀵셀렉트(LEADER+e) 라벨
  quick_select_label_bg = { Color = P.warn },
  quick_select_label_fg = { Color = P.bg },
  quick_select_match_bg = { Color = P.sel_bg },
  quick_select_match_fg = { Color = P.fg },

  tab_bar = {
    -- [실측] 활성탭 글자 4.58 / 비활성탭 글자 4.51 / 배경 구분 4.02
    background = P.tab_bar,
    active_tab = {
      bg_color = P.accent,
      fg_color = P.bg,
      intensity = 'Bold',
    },
    inactive_tab = {
      bg_color = P.tab_bar,
      fg_color = P.tab_ina,
    },
    inactive_tab_hover = {
      bg_color = P.sel_bg,
      fg_color = P.fg,
      italic = false,
    },
    new_tab = {
      bg_color = P.tab_bar,
      fg_color = P.tab_ina,
    },
    new_tab_hover = {
      bg_color = P.accent,
      fg_color = P.bg,
      intensity = 'Bold',
    },
  },
}

-- ---------------------------------------------------------
-- 5. 기본 셸 / 실행 메뉴
-- ---------------------------------------------------------
if is_windows then
  -- PowerShell 7(pwsh)은 별도 설치 항목이다. 설치돼 있을 때만 기본 셸로 쓰고,
  -- 없으면 윈도우에 항상 있는 powershell.exe 로 폴백한다.
  local pwsh = first_existing {
    'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
    'C:\\Program Files (x86)\\PowerShell\\7\\pwsh.exe',
    (os.getenv('LOCALAPPDATA') or '') .. '\\Microsoft\\WindowsApps\\pwsh.exe',
    (os.getenv('ProgramFiles') or '') .. '\\PowerShell\\7\\pwsh.exe',
  }

  if pwsh then
    config.default_prog = { pwsh, '-NoLogo' }
  else
    config.default_prog = { 'powershell.exe', '-NoLogo' }
  end

  config.launch_menu = {
    { label = 'PowerShell 7', args = { 'pwsh.exe', '-NoLogo' } },
    { label = 'Windows PowerShell', args = { 'powershell.exe', '-NoLogo' } },
    { label = 'Command Prompt', args = { 'cmd.exe' } },
    { label = 'WSL', args = { 'wsl.exe', '~' } },
  }
elseif is_linux then
  config.launch_menu = {
    { label = 'bash', args = { 'bash', '-l' } },
    { label = 'zsh', args = { 'zsh', '-l' } },
  }
end

-- ---------------------------------------------------------
-- 6. 키 바인딩
--
--  설계 원칙:
--   (1) 창/탭/패널 조작은 전부 LEADER(Ctrl+A) 아래로 모은다.
--       -> 기본 키와 절대 충돌하지 않는 안전 지대
--   (2) 기본 키로 이미 되는 것은 다시 정의하지 않는다.
--       Ctrl+- / Ctrl+= / Ctrl+0  : 폰트 크기
--       Alt+Enter                 : 전체화면
--       Ctrl+Shift+C / V          : 복사 / 붙여넣기
--       Ctrl+Shift+L              : 디버그 오버레이 (문제 해결용, 덮지 말 것)
--       Ctrl+Shift+P              : 명령 팔레트
--       Ctrl+Shift+Tab / Ctrl+Tab : 탭 이동
--   (3) ALT+문자 는 쓰지 않는다. 셸의 readline / vim 이 Meta 로 사용
-- ---------------------------------------------------------
config.leader = { key = 'a', mods = 'CTRL', timeout_milliseconds = 1000 }

config.keys = {
  ---------------- 패널 분할 ----------------
  { key = '\\', mods = 'LEADER', action = act.SplitHorizontal { domain = 'CurrentPaneDomain' } },
  { key = '-',  mods = 'LEADER', action = act.SplitVertical { domain = 'CurrentPaneDomain' } },

  ---------------- 패널 이동 (vim 키) ----------------
  { key = 'h', mods = 'LEADER', action = act.ActivatePaneDirection 'Left' },
  { key = 'j', mods = 'LEADER', action = act.ActivatePaneDirection 'Down' },
  { key = 'k', mods = 'LEADER', action = act.ActivatePaneDirection 'Up' },
  { key = 'l', mods = 'LEADER', action = act.ActivatePaneDirection 'Right' },

  ---------------- 패널 크기 조정 ----------------
  { key = 'LeftArrow',  mods = 'LEADER', action = act.AdjustPaneSize { 'Left', 5 } },
  { key = 'DownArrow',  mods = 'LEADER', action = act.AdjustPaneSize { 'Down', 5 } },
  { key = 'UpArrow',    mods = 'LEADER', action = act.AdjustPaneSize { 'Up', 5 } },
  { key = 'RightArrow', mods = 'LEADER', action = act.AdjustPaneSize { 'Right', 5 } },

  ---------------- 패널 제어 ----------------
  { key = 'z', mods = 'LEADER', action = act.TogglePaneZoomState },
  { key = 'x', mods = 'LEADER', action = act.CloseCurrentPane { confirm = true } },
  { key = 'q', mods = 'LEADER', action = act.PaneSelect { alphabet = 'asdfghjkl' } },
  { key = 'o', mods = 'LEADER', action = act.RotatePanes 'Clockwise' },

  ---------------- 탭 ----------------
  { key = 'c', mods = 'LEADER', action = act.SpawnTab 'CurrentPaneDomain' },
  { key = 'n', mods = 'LEADER', action = act.ActivateTabRelative(1) },
  { key = 'p', mods = 'LEADER', action = act.ActivateTabRelative(-1) },
  -- '&' 같은 시프트 문자는 키보드 레이아웃에 따라 인식이 불안정하므로 'w' 사용
  { key = 'w', mods = 'LEADER', action = act.CloseCurrentTab { confirm = true } },
  -- 탭 이름 변경. 빈 값으로 확인하면 자동 제목(디렉토리명)으로 되돌아간다.
  { key = ',', mods = 'LEADER', action = act.PromptInputLine {
      description = '탭 이름 (빈 값 = 자동 제목으로 복귀):',
      action = wezterm.action_callback(function(window, _, line)
        if line == nil then return end   -- Esc 로 취소한 경우
        window:active_tab():set_title(line)
      end),
    },
  },

  ---------------- 검색 / 복사 / 선택 ----------------
  { key = '[', mods = 'LEADER', action = act.ActivateCopyMode },
  { key = ']', mods = 'LEADER', action = act.PasteFrom 'Clipboard' },
  { key = 'f', mods = 'LEADER', action = act.Search { CaseInSensitiveString = '' } },
  { key = 'e', mods = 'LEADER', action = act.QuickSelect },
  { key = 'u', mods = 'LEADER', action = act.CharSelect },

  ---------------- 유틸 ----------------
  { key = 'm', mods = 'LEADER', action = act.ShowLauncher },

  -- ---- 작업공간(세션) ----
  -- 탭보다 상위 단위. 프로젝트별로 탭 묶음을 따로 둘 수 있다.
  -- 목록에서 골라 전환 (퍼지 검색 지원)
  { key = 's', mods = 'LEADER', action = act.ShowLauncherArgs {
      flags = 'FUZZY|WORKSPACES', title = '작업공간 선택' } },
  -- 이름을 입력해 새로 만들거나 기존 것으로 전환
  { key = 'S', mods = 'LEADER|SHIFT', action = act.PromptInputLine {
      description = '작업공간 이름 (없으면 새로 생성):',
      action = wezterm.action_callback(function(window, pane, line)
        if line and #line > 0 then
          window:perform_action(act.SwitchToWorkspace { name = line }, pane)
        end
      end),
    },
  },
  -- 현재 작업공간 이름 변경
  -- ';' 사용: '$' 같은 시프트 문자는 키보드 레이아웃에 따라 인식이 불안정하다
  { key = ';', mods = 'LEADER', action = act.PromptInputLine {
      description = '현재 작업공간의 새 이름:',
      action = wezterm.action_callback(function(window, pane, line)
        if line and #line > 0 then
          wezterm.mux.rename_workspace(wezterm.mux.get_active_workspace(), line)
        end
      end),
    },
  },

  -- ---- 탭 관리 ----
  -- 탭이 많아지면 제목만으로 찾기 어렵다. 목록에서 골라 이동한다.
  { key = 't', mods = 'LEADER', action = act.ShowTabNavigator },
  -- 탭 순서 바꾸기 ('<' '>' 같은 시프트 문자는 레이아웃 의존이라 화살표 사용)
  { key = 'LeftArrow',  mods = 'LEADER|SHIFT', action = act.MoveTabRelative(-1) },
  { key = 'RightArrow', mods = 'LEADER|SHIFT', action = act.MoveTabRelative(1) },
  -- [의도적으로 제거] LEADER+X 로 '확인 없이 탭 닫기'를 두었으나 삭제했다.
  --   LEADER+x 는 패널을 '확인 후' 닫는다. 쉬프트 하나 차이로 탭 전체가
  --   확인 없이 사라지면 오조작 한 번에 작업을 잃는다.
  --   탭 닫기는 LEADER+w 하나로 통일한다. 셸만 떠 있을 때는
  --   skip_close_confirmation_for_processes_named 덕분에 확인창이 뜨지 않는다.

  -- [UX] 크기 조절 모드. LEADER 는 1초 뒤 풀리므로 반복 조절이 불편하다.
  --   LEADER+r 로 모드에 들어가면 화살표/hjkl 을 계속 눌러 조절할 수 있고
  --   Esc 또는 Enter 로 빠져나온다. 진행 중에는 상태줄에 모드가 표시된다.
  --   until_unknown=true : 크기조절과 무관한 키를 누르면 모드가 자동으로 풀린다.
  --     이게 없으면 모드에 있는 줄 모르고 명령을 치다가 엉뚱하게 동작한다.
  { key = 'r', mods = 'LEADER', action = act.ActivateKeyTable {
      name = 'resize_pane', one_shot = false,
      timeout_milliseconds = 3000, until_unknown = true } },
  { key = 'K', mods = 'LEADER|SHIFT', action = act.ClearScrollback 'ScrollbackAndViewport' },
  { key = 'R', mods = 'LEADER|SHIFT', action = act.ReloadConfiguration },
  { key = 'd', mods = 'LEADER', action = act.ShowDebugOverlay },

  ---------------- Ctrl+A 원본을 셸로 전달 ----------------
  -- Ctrl+A 를 두 번 누르면 '줄 맨 앞으로 이동'이 정상 동작
  { key = 'a', mods = 'LEADER|CTRL', action = act.SendKey { key = 'a', mods = 'CTRL' } },
}

-- LEADER + 1~9 로 탭 직접 이동
for i = 1, 9 do
  table.insert(config.keys, {
    key = tostring(i),
    mods = 'LEADER',
    action = act.ActivateTab(i - 1),
  })
end

-- ---------------------------------------------------------
-- 7. 마우스 : Ctrl(mac은 Cmd) + 클릭으로 링크 열기
-- ---------------------------------------------------------
local link_mod = is_mac and 'CMD' or 'CTRL'

config.mouse_bindings = {
  {
    event = { Up = { streak = 1, button = 'Left' } },
    mods = link_mod,
    action = act.OpenLinkAtMouseCursor,
  },
  -- 누를 때의 클릭 이벤트가 셸로 새어나가지 않도록 차단
  {
    event = { Down = { streak = 1, button = 'Left' } },
    mods = link_mod,
    action = act.Nop,
  },
}

-- ---------------------------------------------------------
-- 6-2. 모달 키 테이블 : 크기 조절 모드
--   LEADER+r 로 진입. 화살표 또는 hjkl 을 반복해서 누를 수 있다.
--   Esc / Enter / q 로 빠져나오며, 3초간 입력이 없어도 자동 해제된다.
-- ---------------------------------------------------------
config.key_tables = {
  resize_pane = {
    { key = 'LeftArrow',  action = act.AdjustPaneSize { 'Left', 3 } },
    { key = 'h',          action = act.AdjustPaneSize { 'Left', 3 } },
    { key = 'DownArrow',  action = act.AdjustPaneSize { 'Down', 3 } },
    { key = 'j',          action = act.AdjustPaneSize { 'Down', 3 } },
    { key = 'UpArrow',    action = act.AdjustPaneSize { 'Up', 3 } },
    { key = 'k',          action = act.AdjustPaneSize { 'Up', 3 } },
    { key = 'RightArrow', action = act.AdjustPaneSize { 'Right', 3 } },
    { key = 'l',          action = act.AdjustPaneSize { 'Right', 3 } },
    { key = 'Escape', action = 'PopKeyTable' },
    { key = 'Enter',  action = 'PopKeyTable' },
    { key = 'q',      action = 'PopKeyTable' },
  },
}

-- ---------------------------------------------------------
-- 7-1. 상태 표시줄
--   [문제] LEADER(Ctrl+A)를 눌러도 화면에 아무 변화가 없어서
--          눌렸는지 알 수 없고, 모드에 들어가 있는지도 보이지 않는다.
--   [해결] 왼쪽에 LEADER/모드 상태를, 오른쪽에 작업공간과 시간을 표시한다.
-- ---------------------------------------------------------
wezterm.on('update-status', function(window, pane)
  local ok = pcall(function()
    ---------------- 왼쪽: 모드 표시 ----------------
    local left = {}
    local mode = window:active_key_table()

    if mode == 'resize_pane' then
      -- 크기 조절 모드: 주황색으로 강하게
      left = {
        { Background = { Color = P.warn } },
        { Foreground = { Color = P.bg } },
        { Attribute = { Intensity = 'Bold' } },
        { Text = ' 크기조절 hjkl/화살표  Esc 종료 ' },
      }
    elseif window:leader_is_active() then
      -- LEADER 대기 중: 청색
      left = {
        { Background = { Color = P.accent } },
        { Foreground = { Color = P.bg } },
        { Attribute = { Intensity = 'Bold' } },
        { Text = ' LEADER ' },
      }
    else
      left = { { Text = '' } }
    end
    window:set_left_status(wezterm.format(left))

    ---------------- 오른쪽: 작업공간 + 시간 ----------------
    local right = {}
    local ws = window:active_workspace()
    if type(ws) == 'string' and #ws > 0 and ws ~= 'default' then
      -- 이름이 길면 상태줄이 밀려 시간이 화면 밖으로 나간다. 폭 기준으로 자른다.
      if wezterm.column_width(ws) > 16 then
        ws = wezterm.truncate_right(ws, 15) .. '…'
      end
      right[#right + 1] = { Foreground = { Color = P.accent } }
      right[#right + 1] = { Attribute = { Intensity = 'Bold' } }
      right[#right + 1] = { Text = ' ' .. ws .. ' ' }
    end

    -- 확대된 패널이 있으면 알려준다 (확대 중인 걸 모르고 헤매는 일 방지)
    local tab = window:active_tab()
    for _, p in ipairs(tab:panes_with_info()) do
      if p.is_zoomed then
        right[#right + 1] = { Foreground = { Color = P.warn } }
        right[#right + 1] = { Attribute = { Intensity = 'Bold' } }
        right[#right + 1] = { Text = ' 확대중 ' }
        break
      end
    end

    -- [시인성] 이전에는 흐린 회색(#8a7f74)이라 탭바 위에서 대비 2.97 로
    --   거의 읽히지 않았다. 진한 글자(대비 8.80)에 배경 칩을 깔아 분리한다.
    right[#right + 1] = { Background = { Color = P.chip } }
    right[#right + 1] = { Foreground = { Color = P.fg } }
    right[#right + 1] = { Attribute = { Intensity = 'Bold' } }
    right[#right + 1] = { Text = wezterm.strftime('  %m/%d (%a) %H:%M  ') }

    window:set_right_status(wezterm.format(right))
  end)
  if not ok then
    window:set_left_status('')
    window:set_right_status('')
  end
end)

-- ---------------------------------------------------------
-- 7-2. 탭바의 [+] 버튼을 "화면 분할" 버튼으로 바꾼다
--
--   패널 1개일 때 좌클릭 : 좌우 반반으로 2분할
--   패널 2개일 때 좌클릭 : 새 탭 생성 (더 쪼개지 않는다)
--   우클릭               : 기본 동작(런처 메뉴) 유지
--
--   3분할까지 가면 패널 하나의 폭이 좁아 코드와 로그가 접혀 읽기 어렵다.
--   가로 분할은 2개까지만 하고 그 이상은 탭으로 넘긴다.
-- ---------------------------------------------------------
wezterm.on('new-tab-button-click', function(window, pane, button, default_action)
  -- 좌클릭이 아니면 WezTerm 기본 동작에 맡긴다 (우클릭 = 런처 메뉴)
  if button ~= 'Left' then return end

  -- [성능 메모] 분할이 느리게 느껴지는 주된 원인은 이 핸들러가 아니라
  --   새 패널마다 셸 프로세스가 새로 뜨는 비용이다(측정: 핸들러 1회 0.004ms).
  --   Windows PowerShell 은 프로필 로딩 때문에 특히 느리다.
  --   본인 환경 측정:  Measure-Command { pwsh -NoLogo -Command exit }
  --   500ms 를 넘으면 프로필($PROFILE)을 정리하는 게 가장 효과적이다.
  --   아래 FAST_SPLIT 을 true 로 하면 분할된 패널은 프로필 없이 띄운다.
  local FAST_SPLIT = false

  local ok, err = pcall(function()
    local tab = window:active_tab()
    local count = #tab:panes()

    local spawn = nil
    if FAST_SPLIT and is_windows then
      spawn = { args = { config.default_prog[1], '-NoLogo', '-NoProfile' } }
    end

    -- 확대(zoom) 상태에서는 분할이 어색하므로 먼저 해제
    tab:set_zoomed(false)

    if count == 1 then
      -- 좌우 반반. 한 화면을 셋으로 쪼개면 각 폭이 좁아 코드/로그가 접힌다.
      pane:split { direction = 'Right', size = 0.5, args = spawn and spawn.args or nil }
    else
      -- 3분할 이상이면 새 탭
      if default_action then
        window:perform_action(default_action, pane)
      else
        window:perform_action(act.SpawnTab 'CurrentPaneDomain', pane)
      end
    end
  end)

  if not ok then
    wezterm.log_error('new-tab-button-click 실패: ' .. tostring(err))
  end

  -- 처리했음을 알려 기본 동작이 중복 실행되지 않게 한다
  return false
end)

-- ---------------------------------------------------------
-- 8. 탭 제목 : 현재 디렉토리명 표시
--    current_working_dir 은 버전에 따라 Url 객체(userdata) 또는
--    문자열을 반환하므로 양쪽 모두 처리한다
-- ---------------------------------------------------------
wezterm.on('format-tab-title', function(tab)
  -- 이 핸들러는 매 렌더마다 호출된다. 여기서 예외가 나면 탭바 전체가
  -- 그려지지 않으므로 전부 pcall 로 감싸고, 실패 시 최소 제목을 반환한다.
  local ok, items = pcall(function()
    local pane = tab.active_pane

    ---------------- 1. 제목 결정 ----------------
    -- 우선순위: 사용자가 직접 지정한 이름 > 현재 디렉토리명 > 프로세스명
    -- (tab_title 을 보지 않으면 이름을 바꿔도 즉시 덮어써진다)
    local title
    local manual = tab.tab_title
    if manual and #manual > 0 then
      title = manual
    else
      title = pane.title or 'shell'
      local cwd = pane.current_working_dir
      if cwd then
        local path
        if type(cwd) == 'string' then path = cwd else path = cwd.file_path or tostring(cwd) end
        local name = path:gsub('[/\\]+$', ''):match('([^/\\]+)$')
        if name and #name > 0 then title = name end
      end
    end

    ---------------- 2. 상태 수집 ----------------
    -- 수동 이름을 쓰든 자동 제목을 쓰든 상태 표시는 동일하게 붙는다
    local panes, zoomed, unseen = 1, false, false
    if tab.panes then
      panes = #tab.panes
      for _, p in ipairs(tab.panes) do
        if p.is_zoomed then zoomed = true end
        if p.has_unseen_output and not tab.is_active then unseen = true end
      end
    end

    local suffix = ''
    if panes > 1 then suffix = suffix .. ' ' .. panes end
    if zoomed then suffix = suffix .. ' Z' end

    ---------------- 3. 길이 제한 ----------------
    -- 한글은 화면에서 2칸을 차지하므로 열 폭 기준으로 자른다.
    -- 접미사와 여백을 뺀 만큼만 제목에 허용해 닫기(x) 버튼이 잘리지 않게 한다.
    local budget = 22 - wezterm.column_width(suffix)
    if wezterm.column_width(title) > budget then
      title = wezterm.truncate_right(title, budget - 1) .. '…'
    end

    ---------------- 4. 렌더 ----------------
    local out = {}
    if unseen then
      out[#out + 1] = { Foreground = { Color = P.warn } }
      out[#out + 1] = { Text = ' •' }
    else
      out[#out + 1] = { Text = ' ' }
    end
    out[#out + 1] = { Attribute = { Intensity = tab.is_active and 'Bold' or 'Normal' } }
    out[#out + 1] = { Text = string.format(' %d: %s%s ', tab.tab_index + 1, title, suffix) }
    return out
  end)

  if ok and items then return items end
  -- 예외가 나도 탭바가 사라지지 않도록 최소한의 제목을 돌려준다
  return { { Text = string.format(' %d ', (tab.tab_index or 0) + 1) } }
end)

-- ---------------------------------------------------------
-- 9. 시작 시 창 최대화
-- ---------------------------------------------------------
-- [주의] 이 핸들러가 등록되면 WezTerm 은 기본 창을 스스로 만들지 않고
--        이 함수에 위임한다. 여기서 예외가 나면 창이 아예 안 뜨거나
--        기본 창과 겹쳐 두 개가 뜰 수 있으므로 전부 pcall 로 감싼다.
wezterm.on('gui-startup', function(cmd)
  local ok, err = pcall(function()
    -- 이미 창이 있으면 새로 만들지 않는다 (중복 창 방지)
    local existing = wezterm.mux.all_windows()
    if existing and #existing > 0 then
      local gw = existing[1]:gui_window()
      if gw then gw:maximize() end
      return
    end

    local _, _, window = wezterm.mux.spawn_window(cmd or {})
    local gw = window:gui_window()
    if gw then gw:maximize() end
  end)
  if not ok then
    wezterm.log_error('gui-startup 실패: ' .. tostring(err))
  end
end)

return config

+ Recent posts