- Dropped the "☰ Menu" / "Ctrl+Shift+Click / Ctrl+a m" left-status text per request; the menu still opens the same way, it's just no longer labeled in the tab bar. - Dragging a tab out of the fancy tab bar to pop it into its own window turned out not to work reliably on this WezTerm build. Replaced that expectation with an explicit Leader+Enter keybinding that calls pane:move_to_new_window() (documented mux Pane method), a scripted equivalent that doesn't depend on the drag gesture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
930 lines
33 KiB
Lua
930 lines
33 KiB
Lua
local wezterm = require("wezterm")
|
|
local config = wezterm.config_builder() -- 구버전 wezterm이면 config = {} 로 바꾸세요
|
|
local act = wezterm.action
|
|
|
|
-- ============================================================
|
|
-- 폰트 & 테마
|
|
-- ============================================================
|
|
config.font = wezterm.font_with_fallback({
|
|
"JetBrains Mono", -- 원하는 폰트로 교체 (Nerd Font 계열 추천: JetBrainsMono Nerd Font 등)
|
|
"Noto Sans Mono CJK KR", -- 한글 fallback
|
|
})
|
|
config.font_size = 13.0
|
|
config.line_height = 1.1
|
|
|
|
config.color_scheme = "Catppuccin Mocha" -- wezterm show-config 나 https://wezterm.org/colorschemes/ 에서 이름 확인
|
|
-- 색상표를 직접 지정하고 싶으면 color_scheme 대신 아래처럼:
|
|
-- config.colors = {
|
|
-- foreground = "#c0caf5",
|
|
-- background = "#1a1b26",
|
|
-- cursor_bg = "#c0caf5",
|
|
-- cursor_border = "#c0caf5",
|
|
-- }
|
|
|
|
config.window_background_opacity = 0.92 -- 1.0이면 완전 불투명
|
|
config.macos_window_background_blur = 20 -- macOS 전용, 배경 블러
|
|
config.text_background_opacity = 1.0
|
|
|
|
-- ============================================================
|
|
-- 창 & 탭바 레이아웃
|
|
-- ============================================================
|
|
config.window_padding = {
|
|
left = 8,
|
|
right = 8,
|
|
top = 8,
|
|
bottom = 8,
|
|
}
|
|
config.initial_cols = 140
|
|
config.initial_rows = 40
|
|
|
|
config.use_fancy_tab_bar = true -- OS 스타일 탭바: 탭에 마우스 올리면 닫기(x) 버튼이 보이고, 탭을 밖으로 드래그하면 새 창으로 분리됨
|
|
config.tab_bar_at_bottom = false -- 탭바/상태바를 터미널 상단에 표시
|
|
config.hide_tab_bar_if_only_one_tab = false -- 탭이 하나뿐이어도 탭바를 항상 표시
|
|
config.show_new_tab_button_in_tab_bar = true
|
|
config.window_decorations = "INTEGRATED_BUTTONS | RESIZE" -- OS 타이틀바 대신 탭바에 최소화/최대화/닫기 버튼을 통합 표시
|
|
|
|
-- ------------------------------------------------------------
|
|
-- [MobaXterm 스타일] 호스트(도메인)별 탭 색상 자동 구분
|
|
-- 같은 SSH 호스트/도메인에서 연 탭은 항상 같은 색으로 표시됨
|
|
-- ------------------------------------------------------------
|
|
local host_colors = {
|
|
"#f38ba8", "#fab387", "#f9e2af", "#a6e3a1",
|
|
"#94e2d5", "#89b4fa", "#cba6f7", "#f5c2e7",
|
|
}
|
|
|
|
local function color_for_host(name)
|
|
local hash = 0
|
|
for i = 1, #name do
|
|
hash = (hash * 31 + string.byte(name, i)) % 100000
|
|
end
|
|
return host_colors[(hash % #host_colors) + 1]
|
|
end
|
|
|
|
-- ------------------------------------------------------------
|
|
-- [1] 탭 제목 커스터마이징
|
|
-- 탭에 "인덱스: 실행중인 프로세스명 [호스트]" 형식으로 표시
|
|
-- local(로컬 셸)이면 회색조, SSH 도메인이면 호스트별 고유 색
|
|
-- ------------------------------------------------------------
|
|
wezterm.on("format-tab-title", function(tab, tabs, panes, config, hover, max_width)
|
|
local process = tab.active_pane.foreground_process_name or ""
|
|
local name = process:match("([^/\\]+)$") or process
|
|
if name == "" then
|
|
name = tab.active_pane.title
|
|
end
|
|
local domain = tab.active_pane.domain_name or "local"
|
|
local title = " " .. name .. " "
|
|
|
|
local accent = domain == "local" and "#6c7086" or color_for_host(domain)
|
|
if tab.is_active then
|
|
return {
|
|
{ Background = { Color = accent } },
|
|
{ Foreground = { Color = "#1e1e2e" } },
|
|
{ Text = title },
|
|
}
|
|
end
|
|
return {
|
|
{ Foreground = { Color = accent } },
|
|
{ Text = title },
|
|
}
|
|
end)
|
|
|
|
-- ------------------------------------------------------------
|
|
-- [MobaXterm 스타일] 우측 상태바: 접속 호스트 + 워크스페이스 + 접속 경과시간 + 날짜/시간
|
|
-- MobaXterm 하단 상태바처럼 "지금 어디에 붙어있는지"를 한눈에 표시
|
|
-- ------------------------------------------------------------
|
|
local pane_start_times = {}
|
|
|
|
wezterm.on("update-right-status", function(window, pane)
|
|
local pane_id = pane:pane_id()
|
|
if not pane_start_times[pane_id] then
|
|
pane_start_times[pane_id] = os.time()
|
|
end
|
|
local elapsed = os.time() - pane_start_times[pane_id]
|
|
local elapsed_str =
|
|
string.format("%02d:%02d:%02d", elapsed // 3600, (elapsed % 3600) // 60, elapsed % 60)
|
|
|
|
local domain = pane:get_domain_name() or "local"
|
|
local workspace = window:active_workspace()
|
|
local date = wezterm.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
window:set_right_status(wezterm.format({
|
|
{ Foreground = { Color = color_for_host(domain) } },
|
|
{ Text = " " .. domain .. " " },
|
|
{ Foreground = { Color = "#89b4fa" } },
|
|
{ Text = "| " .. workspace .. " " },
|
|
{ Foreground = { Color = "#f9e2af" } },
|
|
{ Text = "| " .. elapsed_str .. " " },
|
|
{ Foreground = { Color = "#cdd6f4" } },
|
|
{ Text = "| " .. date .. " " },
|
|
}))
|
|
end)
|
|
|
|
-- ============================================================
|
|
-- [2] 세션 / 멀티플렉싱
|
|
-- ============================================================
|
|
-- SSH 도메인: 자주 접속하는 원격 서버를 wezterm이 네이티브로 관리
|
|
-- 실제 서버 정보가 준비되면 아래 예시를 복사해서 채우세요.
|
|
config.ssh_domains = {
|
|
-- {
|
|
-- name = "myserver",
|
|
-- remote_address = "myserver.example.com",
|
|
-- username = "youruser",
|
|
-- },
|
|
}
|
|
|
|
-- Unix domain (mux server): wezterm을 서버로 띄워두면 터미널 껐다 켜도 세션 유지
|
|
-- 사용법: 먼저 `wezterm-mux-server --daemonize` 실행 후, 클라이언트에서 아래 도메인에 접속
|
|
config.unix_domains = {
|
|
{ name = "unix" },
|
|
}
|
|
|
|
local is_windows = wezterm.target_triple:find("windows") ~= nil
|
|
local config_file = wezterm.config_file or (is_windows and (wezterm.home_dir .. "\\.wezterm.lua") or "/mnt/storage/wezterm.lua")
|
|
|
|
local function notify(window, message, duration_ms)
|
|
if window.toast_notification then
|
|
window:toast_notification("WezTerm", message, nil, duration_ms or 4000)
|
|
else
|
|
wezterm.log_info(message)
|
|
end
|
|
end
|
|
|
|
local function read_config_text()
|
|
local file = io.open(config_file, "r")
|
|
if not file then
|
|
return nil, "설정 파일을 열 수 없습니다: " .. config_file
|
|
end
|
|
local text = file:read("*a")
|
|
file:close()
|
|
return text
|
|
end
|
|
|
|
local function write_config_text(text)
|
|
local file = io.open(config_file, "w")
|
|
if not file then
|
|
return false, "설정 파일에 쓸 수 없습니다: " .. config_file
|
|
end
|
|
file:write(text)
|
|
file:close()
|
|
return true
|
|
end
|
|
|
|
local function set_config_value(window, pane, key, value)
|
|
local text, err = read_config_text()
|
|
if not text then
|
|
notify(window, err)
|
|
return
|
|
end
|
|
|
|
local pattern = "(config%." .. key .. "%s*=%s*)[^\n]+"
|
|
local updated, count = text:gsub(pattern, function(prefix)
|
|
return prefix .. value
|
|
end, 1)
|
|
|
|
if count == 0 then
|
|
notify(window, "설정 항목을 찾지 못했습니다: config." .. key)
|
|
return
|
|
end
|
|
|
|
local ok, write_err = write_config_text(updated)
|
|
if not ok then
|
|
notify(window, write_err)
|
|
return
|
|
end
|
|
|
|
notify(window, "저장됨: config." .. key .. " = " .. value)
|
|
window:perform_action(act.ReloadConfiguration, pane)
|
|
end
|
|
|
|
local function prompt_config_value(window, pane, title, key, initial_value, quote_string)
|
|
window:perform_action(
|
|
act.PromptInputLine({
|
|
description = title .. " (현재: " .. tostring(initial_value) .. ")",
|
|
action = wezterm.action_callback(function(cb_window, cb_pane, line)
|
|
if not line or line == "" then
|
|
return
|
|
end
|
|
local value = quote_string and string.format("%q", line) or line
|
|
set_config_value(cb_window, cb_pane, key, value)
|
|
end),
|
|
}),
|
|
pane
|
|
)
|
|
end
|
|
|
|
-- config.font = wezterm.font_with_fallback({ "영어 폰트", "한글 폰트" }) 블록에서
|
|
-- 현재 지정된 두 폰트 이름을 읽어온다 (설정 파일 텍스트 기준, 실행 중인 config 객체가 아님)
|
|
local function get_current_fonts()
|
|
local text = read_config_text()
|
|
if not text then
|
|
return nil, nil
|
|
end
|
|
|
|
local block = text:match("config%.font = wezterm%.font_with_fallback%(%{(.-)\n%}%)")
|
|
if not block then
|
|
return nil, nil
|
|
end
|
|
|
|
local fonts = {}
|
|
for f in block:gmatch('"([^"]*)"') do
|
|
table.insert(fonts, f)
|
|
end
|
|
return fonts[1], fonts[2]
|
|
end
|
|
|
|
local function set_fonts(window, pane, primary, korean)
|
|
local text, err = read_config_text()
|
|
if not text then
|
|
notify(window, err)
|
|
return
|
|
end
|
|
|
|
local new_block = string.format('\n\t%q, -- 영어/기본 폰트\n\t%q, -- 한글 폰트\n', primary, korean)
|
|
local safe_block = new_block:gsub("%%", "%%%%")
|
|
local updated, count = text:gsub("(config%.font = wezterm%.font_with_fallback%(%{).-\n(%}%))", "%1" .. safe_block .. "%2", 1)
|
|
|
|
if count == 0 then
|
|
notify(window, "config.font 설정을 찾지 못했습니다")
|
|
return
|
|
end
|
|
|
|
local ok, write_err = write_config_text(updated)
|
|
if not ok then
|
|
notify(window, write_err)
|
|
return
|
|
end
|
|
|
|
notify(window, "폰트 저장됨: 영어=" .. primary .. ", 한글=" .. korean)
|
|
window:perform_action(act.ReloadConfiguration, pane)
|
|
end
|
|
|
|
local function prompt_font(window, pane, which)
|
|
local primary, korean = get_current_fonts()
|
|
primary = primary or "JetBrains Mono"
|
|
korean = korean or "Noto Sans Mono CJK KR"
|
|
|
|
local current = which == "primary" and primary or korean
|
|
local title = (which == "primary" and "영어/기본 폰트" or "한글 폰트") .. " (현재: " .. current .. ")"
|
|
|
|
window:perform_action(
|
|
act.PromptInputLine({
|
|
description = title,
|
|
action = wezterm.action_callback(function(cb_window, cb_pane, line)
|
|
if not line or line == "" then
|
|
return
|
|
end
|
|
if which == "primary" then
|
|
set_fonts(cb_window, cb_pane, line, korean)
|
|
else
|
|
set_fonts(cb_window, cb_pane, primary, line)
|
|
end
|
|
end),
|
|
}),
|
|
pane
|
|
)
|
|
end
|
|
|
|
-- `wezterm ls-fonts --list-system`는 동기(블로킹) 호출이라 매번 실행하면 메뉴 전환 사이에
|
|
-- 밑에 있던 셸이 잠깐 보이는 깜빡임이 생긴다. 한 번 조회한 결과를 캐싱해 재사용한다
|
|
local system_fonts_cache = nil
|
|
|
|
local function list_system_fonts()
|
|
if system_fonts_cache then
|
|
return system_fonts_cache
|
|
end
|
|
|
|
local exe = is_windows and (wezterm.executable_dir .. "\\wezterm.exe") or (wezterm.executable_dir .. "/wezterm")
|
|
local ok, stdout = wezterm.run_child_process({ exe, "ls-fonts", "--list-system" })
|
|
if not ok or not stdout then
|
|
return nil
|
|
end
|
|
|
|
-- 실제 출력은 `wezterm.font("이름", {weight=...}) -- (AKA: ...) 경로, DirectWrite` 형태의
|
|
-- 코드 스니펫이라, 그 안의 family 이름만 뽑아낸다. 이 패턴에 안 맞는 줄은 그대로 버린다
|
|
-- (예전에는 안 맞는 줄을 통째로 "이름"으로 취급해서, 그 스니펫 텍스트 안에 있던 "})"가
|
|
-- config.font 블록 편집 시 조기 종료를 일으켜 설정 파일이 깨지는 사고가 있었다)
|
|
local seen = {}
|
|
local fonts = {}
|
|
for line in stdout:gmatch("[^\r\n]+") do
|
|
local name = line:match('wezterm%.font%("([^"]+)"') or line:match('^"([^"]+)"')
|
|
if name and name ~= "" and not seen[name] then
|
|
seen[name] = true
|
|
table.insert(fonts, name)
|
|
end
|
|
end
|
|
table.sort(fonts)
|
|
system_fonts_cache = fonts
|
|
return fonts
|
|
end
|
|
|
|
local function show_font_picker(window, pane, which)
|
|
local primary, korean = get_current_fonts()
|
|
primary = primary or "JetBrains Mono"
|
|
korean = korean or "Noto Sans Mono CJK KR"
|
|
|
|
local fonts = list_system_fonts()
|
|
if not fonts or #fonts == 0 then
|
|
notify(window, "설치된 폰트 목록을 가져오지 못했습니다. 직접 입력으로 진행합니다")
|
|
prompt_font(window, pane, which)
|
|
return
|
|
end
|
|
|
|
local choices = { { label = "직접 입력...", id = "__manual__" } }
|
|
for _, name in ipairs(fonts) do
|
|
table.insert(choices, { label = name, id = name })
|
|
end
|
|
|
|
local current = which == "primary" and primary or korean
|
|
|
|
window:perform_action(
|
|
act.InputSelector({
|
|
title = (which == "primary" and "영어/기본 폰트" or "한글 폰트") .. " 선택 (현재: " .. current .. ")",
|
|
fuzzy = true,
|
|
choices = choices,
|
|
action = wezterm.action_callback(function(inner_window, inner_pane, id)
|
|
if not id then
|
|
return
|
|
end
|
|
if id == "__manual__" then
|
|
prompt_font(inner_window, inner_pane, which)
|
|
return
|
|
end
|
|
if which == "primary" then
|
|
set_fonts(inner_window, inner_pane, id, korean)
|
|
else
|
|
set_fonts(inner_window, inner_pane, primary, id)
|
|
end
|
|
end),
|
|
}),
|
|
pane
|
|
)
|
|
end
|
|
|
|
local function show_font_settings(window, pane)
|
|
window:perform_action(
|
|
act.InputSelector({
|
|
title = "폰트 설정",
|
|
fuzzy = true,
|
|
choices = {
|
|
{ label = "폰트 크기", id = "font_size" },
|
|
{ label = "영어 폰트", id = "font_primary" },
|
|
{ label = "한글 폰트", id = "font_korean" },
|
|
{ label = "줄 높이", id = "line_height" },
|
|
{ label = "색상 테마", id = "color_scheme" },
|
|
{ label = "창 투명도", id = "opacity" },
|
|
},
|
|
action = wezterm.action_callback(function(inner_window, inner_pane, id)
|
|
if id == "font_size" then
|
|
prompt_config_value(inner_window, inner_pane, "폰트 크기", "font_size", config.font_size, false)
|
|
elseif id == "font_primary" then
|
|
show_font_picker(inner_window, inner_pane, "primary")
|
|
elseif id == "font_korean" then
|
|
show_font_picker(inner_window, inner_pane, "korean")
|
|
elseif id == "line_height" then
|
|
prompt_config_value(inner_window, inner_pane, "줄 높이", "line_height", config.line_height, false)
|
|
elseif id == "color_scheme" then
|
|
prompt_config_value(inner_window, inner_pane, "색상 테마", "color_scheme", config.color_scheme, true)
|
|
elseif id == "opacity" then
|
|
prompt_config_value(inner_window, inner_pane, "창 투명도", "window_background_opacity", config.window_background_opacity, false)
|
|
end
|
|
end),
|
|
}),
|
|
pane
|
|
)
|
|
end
|
|
|
|
local function escape_pattern(s)
|
|
return (s:gsub("[%^%$%(%)%.%[%]%*%+%-%?%%]", "%%%1"))
|
|
end
|
|
|
|
-- "host:port" 형태의 remote_address를 host, port로 분리 (port 없으면 port는 nil)
|
|
local function split_remote_address(remote_address)
|
|
local host, port = remote_address:match("^(.-):(%d+)$")
|
|
if host then
|
|
return host, port
|
|
end
|
|
return remote_address, nil
|
|
end
|
|
|
|
local function append_ssh_domain(window, pane, name, remote_address, username)
|
|
local text, err = read_config_text()
|
|
if not text then
|
|
notify(window, err)
|
|
return
|
|
end
|
|
|
|
local entry = string.format(
|
|
'\t{\n\t\tname = %q,\n\t\tremote_address = %q,\n\t\tusername = %q,\n\t\tmultiplexing = "None",\n\t},\n',
|
|
name,
|
|
remote_address,
|
|
username
|
|
)
|
|
local updated, count = text:gsub("\n}%s*\n\n%-%- Unix domain", "\n" .. entry .. "}\n\n-- Unix domain", 1)
|
|
|
|
if count == 0 then
|
|
notify(window, "config.ssh_domains 끝 위치를 찾지 못했습니다")
|
|
return
|
|
end
|
|
|
|
local ok, write_err = write_config_text(updated)
|
|
if not ok then
|
|
notify(window, write_err)
|
|
return
|
|
end
|
|
|
|
notify(
|
|
window,
|
|
"저장됨: " .. name .. " -> " .. remote_address .. "\n"
|
|
.. "⚠ 지금 바로 연결하면 실패합니다. 작업 관리자에서 wezterm-gui.exe를 완전히 종료 후 재시작하세요.",
|
|
10000
|
|
)
|
|
window:perform_action(act.ReloadConfiguration, pane)
|
|
end
|
|
|
|
local function update_ssh_domain(window, pane, old_name, name, remote_address, username)
|
|
local text, err = read_config_text()
|
|
if not text then
|
|
notify(window, err)
|
|
return
|
|
end
|
|
|
|
local quoted_old_name = escape_pattern(string.format("%q", old_name))
|
|
local pattern = "\t{\n\t\tname = " .. quoted_old_name .. ",\n.-\n\t},\n"
|
|
local entry = string.format(
|
|
'\t{\n\t\tname = %q,\n\t\tremote_address = %q,\n\t\tusername = %q,\n\t\tmultiplexing = "None",\n\t},\n',
|
|
name,
|
|
remote_address,
|
|
username
|
|
)
|
|
local safe_entry = entry:gsub("%%", "%%%%")
|
|
|
|
local updated, count = text:gsub(pattern, safe_entry, 1)
|
|
if count == 0 then
|
|
notify(window, "SSH 도메인 항목을 찾지 못했습니다: " .. old_name)
|
|
return
|
|
end
|
|
|
|
local ok, write_err = write_config_text(updated)
|
|
if not ok then
|
|
notify(window, write_err)
|
|
return
|
|
end
|
|
|
|
notify(
|
|
window,
|
|
"수정됨: " .. name .. " -> " .. remote_address .. "\n"
|
|
.. "⚠ 지금 바로 연결하면 실패합니다. 작업 관리자에서 wezterm-gui.exe를 완전히 종료 후 재시작하세요.",
|
|
10000
|
|
)
|
|
window:perform_action(act.ReloadConfiguration, pane)
|
|
end
|
|
|
|
-- SSH 도메인 등록을 이름 -> 주소 -> 포트 -> 사용자명 순서로 한 칸씩 물어봄
|
|
-- (WezTerm에는 입력칸을 여러 개 한 화면에 배치하는 폼 위젯이 없어, 한 줄짜리
|
|
-- PromptInputLine을 단계별로 이어서 띄우는 방식으로 흉내낸다)
|
|
local ssh_domain_steps = {
|
|
{ field = "name", title = "SSH 이름 (1/4, 예: prod)" },
|
|
{ field = "host", title = "SSH 주소 (2/4, 예: myserver.example.com)" },
|
|
{ field = "port", title = "SSH 포트 (3/4, 비워두면 기본값 22)", optional = true },
|
|
{ field = "username", title = "SSH 사용자명 (4/4, 예: ubuntu)" },
|
|
}
|
|
|
|
local function prompt_ssh_domain_step(window, pane, step_index, data)
|
|
local step = ssh_domain_steps[step_index]
|
|
|
|
window:perform_action(
|
|
act.PromptInputLine({
|
|
description = step.title,
|
|
action = wezterm.action_callback(function(cb_window, cb_pane, line)
|
|
if (not line or line == "") and not step.optional then
|
|
notify(cb_window, "SSH 도메인 추가를 취소했습니다")
|
|
return
|
|
end
|
|
|
|
data[step.field] = line or ""
|
|
|
|
if step_index < #ssh_domain_steps then
|
|
prompt_ssh_domain_step(cb_window, cb_pane, step_index + 1, data)
|
|
else
|
|
local remote_address = data.host
|
|
if data.port ~= "" then
|
|
remote_address = remote_address .. ":" .. data.port
|
|
end
|
|
append_ssh_domain(cb_window, cb_pane, data.name, remote_address, data.username)
|
|
end
|
|
end),
|
|
}),
|
|
pane
|
|
)
|
|
end
|
|
|
|
local function prompt_ssh_domain(window, pane)
|
|
prompt_ssh_domain_step(window, pane, 1, {})
|
|
end
|
|
|
|
-- SSH 도메인 수정: 기존 값은 비워두면 그대로 유지, 입력하면 해당 항목만 교체
|
|
local edit_ssh_domain_steps = {
|
|
{ field = "name", label = "SSH 이름" },
|
|
{ field = "host", label = "SSH 주소" },
|
|
{ field = "port", label = "SSH 포트" },
|
|
{ field = "username", label = "SSH 사용자명" },
|
|
}
|
|
|
|
local function prompt_edit_ssh_domain_step(window, pane, step_index, data)
|
|
local step = edit_ssh_domain_steps[step_index]
|
|
local current_display = data[step.field]
|
|
if step.field == "port" and current_display == "" then
|
|
current_display = "기본값 22"
|
|
end
|
|
|
|
window:perform_action(
|
|
act.PromptInputLine({
|
|
description = string.format(
|
|
"%s (%d/%d, 현재: %s, 비워두면 유지)",
|
|
step.label,
|
|
step_index,
|
|
#edit_ssh_domain_steps,
|
|
current_display
|
|
),
|
|
action = wezterm.action_callback(function(cb_window, cb_pane, line)
|
|
if line and line ~= "" then
|
|
data[step.field] = line
|
|
end
|
|
|
|
if step_index < #edit_ssh_domain_steps then
|
|
prompt_edit_ssh_domain_step(cb_window, cb_pane, step_index + 1, data)
|
|
else
|
|
local remote_address = data.host
|
|
if data.port ~= "" then
|
|
remote_address = remote_address .. ":" .. data.port
|
|
end
|
|
update_ssh_domain(cb_window, cb_pane, data.original_name, data.name, remote_address, data.username)
|
|
end
|
|
end),
|
|
}),
|
|
pane
|
|
)
|
|
end
|
|
|
|
local function prompt_edit_ssh_domain(window, pane, domain)
|
|
local host, port = split_remote_address(domain.remote_address)
|
|
prompt_edit_ssh_domain_step(window, pane, 1, {
|
|
original_name = domain.name,
|
|
name = domain.name,
|
|
host = host,
|
|
port = port or "",
|
|
username = domain.username,
|
|
})
|
|
end
|
|
|
|
local function show_edit_ssh_domain_list(window, pane)
|
|
local choices = {}
|
|
for _, domain in ipairs(config.ssh_domains or {}) do
|
|
table.insert(choices, {
|
|
label = domain.name .. " (" .. domain.remote_address .. ")",
|
|
id = domain.name,
|
|
})
|
|
end
|
|
|
|
if #choices == 0 then
|
|
notify(window, "등록된 SSH 도메인이 없습니다")
|
|
return
|
|
end
|
|
|
|
window:perform_action(
|
|
act.InputSelector({
|
|
title = "SSH 도메인 수정",
|
|
fuzzy = true,
|
|
choices = choices,
|
|
action = wezterm.action_callback(function(inner_window, inner_pane, id)
|
|
if not id then
|
|
return
|
|
end
|
|
for _, domain in ipairs(config.ssh_domains or {}) do
|
|
if domain.name == id then
|
|
prompt_edit_ssh_domain(inner_window, inner_pane, domain)
|
|
return
|
|
end
|
|
end
|
|
end),
|
|
}),
|
|
pane
|
|
)
|
|
end
|
|
|
|
local function show_connection_settings(window, pane)
|
|
window:perform_action(
|
|
act.InputSelector({
|
|
title = "연결 설정",
|
|
fuzzy = true,
|
|
choices = {
|
|
{ label = "SSH 도메인 추가", id = "add_ssh" },
|
|
{ label = "SSH 도메인 수정", id = "edit_ssh" },
|
|
{ label = "SSH 도메인 목록", id = "domains" },
|
|
{ label = "통합 런처", id = "launcher" },
|
|
},
|
|
action = wezterm.action_callback(function(inner_window, inner_pane, id)
|
|
if id == "add_ssh" then
|
|
prompt_ssh_domain(inner_window, inner_pane)
|
|
elseif id == "edit_ssh" then
|
|
show_edit_ssh_domain_list(inner_window, inner_pane)
|
|
elseif id == "domains" then
|
|
inner_window:perform_action(act.ShowLauncherArgs({ flags = "DOMAINS" }), inner_pane)
|
|
elseif id == "launcher" then
|
|
inner_window:perform_action(act.ShowLauncherArgs({ flags = "FUZZY|TABS|DOMAINS|WORKSPACES|LAUNCH_MENU_ITEMS" }), inner_pane)
|
|
end
|
|
end),
|
|
}),
|
|
pane
|
|
)
|
|
end
|
|
|
|
local function show_other_settings(window, pane)
|
|
window:perform_action(
|
|
act.InputSelector({
|
|
title = "기타 설정",
|
|
fuzzy = true,
|
|
choices = {
|
|
{ label = "스크롤백 줄 수", id = "scrollback" },
|
|
{ label = "최대 FPS", id = "max_fps" },
|
|
{ label = "탭바 위치: 상단", id = "tab_top" },
|
|
{ label = "탭바 위치: 하단", id = "tab_bottom" },
|
|
{ label = "창 닫기 확인 켜기", id = "close_prompt" },
|
|
{ label = "창 닫기 확인 끄기", id = "close_no_prompt" },
|
|
},
|
|
action = wezterm.action_callback(function(inner_window, inner_pane, id)
|
|
if id == "scrollback" then
|
|
prompt_config_value(inner_window, inner_pane, "스크롤백 줄 수", "scrollback_lines", config.scrollback_lines, false)
|
|
elseif id == "max_fps" then
|
|
prompt_config_value(inner_window, inner_pane, "최대 FPS", "max_fps", config.max_fps, false)
|
|
elseif id == "tab_top" then
|
|
set_config_value(inner_window, inner_pane, "tab_bar_at_bottom", "false")
|
|
elseif id == "tab_bottom" then
|
|
set_config_value(inner_window, inner_pane, "tab_bar_at_bottom", "true")
|
|
elseif id == "close_prompt" then
|
|
set_config_value(inner_window, inner_pane, "window_close_confirmation", string.format("%q", "AlwaysPrompt"))
|
|
elseif id == "close_no_prompt" then
|
|
set_config_value(inner_window, inner_pane, "window_close_confirmation", string.format("%q", "NeverPrompt"))
|
|
end
|
|
end),
|
|
}),
|
|
pane
|
|
)
|
|
end
|
|
|
|
local function show_settings_menu(window, pane)
|
|
window:perform_action(
|
|
act.InputSelector({
|
|
title = "설정",
|
|
fuzzy = true,
|
|
choices = {
|
|
{ label = "폰트 설정", id = "font" },
|
|
{ label = "연결 설정", id = "connection" },
|
|
{ label = "기타 설정", id = "other" },
|
|
{ label = "통합 런처", id = "launcher" },
|
|
{ label = "설정 다시 불러오기", id = "reload" },
|
|
},
|
|
action = wezterm.action_callback(function(inner_window, inner_pane, id)
|
|
if not id then
|
|
return
|
|
end
|
|
|
|
if id == "font" then
|
|
show_font_settings(inner_window, inner_pane)
|
|
elseif id == "connection" then
|
|
show_connection_settings(inner_window, inner_pane)
|
|
elseif id == "other" then
|
|
show_other_settings(inner_window, inner_pane)
|
|
elseif id == "launcher" then
|
|
inner_window:perform_action(
|
|
act.ShowLauncherArgs({ flags = "FUZZY|TABS|DOMAINS|WORKSPACES|LAUNCH_MENU_ITEMS" }),
|
|
inner_pane
|
|
)
|
|
elseif id == "reload" then
|
|
inner_window:perform_action(act.ReloadConfiguration, inner_pane)
|
|
end
|
|
end),
|
|
}),
|
|
pane
|
|
)
|
|
end
|
|
|
|
-- 메인 메뉴: 등록된 SSH 연결들을 맨 위에, 설정 항목은 맨 아래에 배치
|
|
local function show_main_menu(window, pane)
|
|
local choices = {}
|
|
|
|
for _, domain in ipairs(config.ssh_domains or {}) do
|
|
table.insert(choices, {
|
|
label = domain.name .. " (" .. domain.remote_address .. ")",
|
|
id = "domain:" .. domain.name,
|
|
})
|
|
end
|
|
|
|
table.insert(choices, { label = "설정", id = "settings" })
|
|
|
|
window:perform_action(
|
|
act.InputSelector({
|
|
title = "메인 메뉴",
|
|
fuzzy = true,
|
|
choices = choices,
|
|
action = wezterm.action_callback(function(inner_window, inner_pane, id)
|
|
if not id then
|
|
return
|
|
end
|
|
|
|
local domain_name = id:match("^domain:(.+)$")
|
|
if domain_name then
|
|
inner_window:perform_action(act.SpawnTab({ DomainName = domain_name }), inner_pane)
|
|
elseif id == "settings" then
|
|
show_settings_menu(inner_window, inner_pane)
|
|
end
|
|
end),
|
|
}),
|
|
pane
|
|
)
|
|
end
|
|
|
|
wezterm.on("show-main-menu", function(window, pane)
|
|
show_main_menu(window, pane)
|
|
end)
|
|
|
|
|
|
-- 시작 시 첫 화면으로 메인 메뉴를 띄워 설정과 연결 메뉴에 바로 접근합니다.
|
|
wezterm.on("gui-startup", function(cmd)
|
|
local tab, pane, window = wezterm.mux.spawn_window(cmd or {})
|
|
list_system_fonts() -- 폰트 목록을 미리 캐싱해서, 나중에 폰트 설정 메뉴에서 조회 지연으로 화면이 깜빡이지 않게 함
|
|
show_main_menu(window:gui_window(), pane)
|
|
end)
|
|
|
|
-- 시작 시 자동으로 워크스페이스/레이아웃을 구성하려면 아래 예시를 참고하세요.
|
|
-- wezterm.on("gui-startup", function(cmd)
|
|
-- local tab, pane, window = wezterm.mux.spawn_window(cmd or {})
|
|
-- pane:split({ direction = "Right", size = 0.3 })
|
|
-- end)
|
|
|
|
-- 워크스페이스 전환 (tmux 세션 개념) : leader+w 로 목록에서 선택/생성
|
|
-- (아래 keys 테이블에 SwitchToWorkspace, ShowLauncherArgs 바인딩 있음)
|
|
|
|
-- ============================================================
|
|
-- 키바인딩 (Leader key: tmux 스타일)
|
|
-- ============================================================
|
|
config.leader = { key = "a", mods = "CTRL", timeout_milliseconds = 1000 }
|
|
|
|
config.keys = {
|
|
-- 탭 (Windows 관례: Ctrl+Shift+*, 셸 시그널/기존 단축키와 충돌 피함)
|
|
{ key = "t", mods = "CTRL|SHIFT", action = act.SpawnTab("CurrentPaneDomain") },
|
|
{ key = "w", mods = "CTRL|SHIFT", action = act.CloseCurrentTab({ confirm = true }) },
|
|
{ key = "1", mods = "CTRL|SHIFT", action = act.ActivateTab(0) },
|
|
{ key = "2", mods = "CTRL|SHIFT", action = act.ActivateTab(1) },
|
|
{ key = "3", mods = "CTRL|SHIFT", action = act.ActivateTab(2) },
|
|
|
|
-- 창 분할 (leader + | / -)
|
|
{ key = "\\", mods = "LEADER", action = act.SplitHorizontal({ domain = "CurrentPaneDomain" }) },
|
|
{ key = "-", mods = "LEADER", action = act.SplitVertical({ domain = "CurrentPaneDomain" }) },
|
|
|
|
-- 패인(pane) 이동: leader + hjkl
|
|
{ key = "h", mods = "LEADER", action = act.ActivatePaneDirection("Left") },
|
|
{ key = "l", mods = "LEADER", action = act.ActivatePaneDirection("Right") },
|
|
{ key = "k", mods = "LEADER", action = act.ActivatePaneDirection("Up") },
|
|
{ key = "j", mods = "LEADER", action = act.ActivatePaneDirection("Down") },
|
|
|
|
-- 패인 닫기 / 전체화면 토글
|
|
{ key = "x", mods = "LEADER", action = act.CloseCurrentPane({ confirm = true }) },
|
|
{ key = "z", mods = "LEADER", action = act.TogglePaneZoomState },
|
|
|
|
-- 폰트 크기 조절 (브라우저와 동일한 관례: Ctrl+=/-/0)
|
|
{ key = "=", mods = "CTRL", action = act.IncreaseFontSize },
|
|
{ key = "-", mods = "CTRL", action = act.DecreaseFontSize },
|
|
{ key = "0", mods = "CTRL", action = act.ResetFontSize },
|
|
|
|
-- 복사/붙여넣기 (Ctrl+C/V는 셸 시그널/붙여넣기와 겹치므로 Shift 추가)
|
|
{ key = "c", mods = "CTRL|SHIFT", action = act.CopyTo("Clipboard") },
|
|
{ key = "v", mods = "CTRL|SHIFT", action = act.PasteFrom("Clipboard") },
|
|
|
|
-- 메인 메뉴: 설정 메뉴와 통합 런처를 다시 표시
|
|
{ key = "m", mods = "LEADER", action = act.EmitEvent("show-main-menu") },
|
|
|
|
-- 현재 탭을 새 창으로 분리 (탭바 드래그 대신 확실하게 동작하는 방법)
|
|
{
|
|
key = "Enter",
|
|
mods = "LEADER",
|
|
action = wezterm.action_callback(function(window, pane)
|
|
pane:move_to_new_window()
|
|
end),
|
|
},
|
|
|
|
-- [2] 워크스페이스(=tmux 세션) 전환/생성 목록
|
|
{ key = "w", mods = "LEADER", action = act.ShowLauncherArgs({ flags = "WORKSPACES" }) },
|
|
-- [2] SSH 도메인 접속 (config.ssh_domains에 등록한 서버 목록에서 선택)
|
|
{ key = "s", mods = "LEADER", action = act.ShowLauncherArgs({ flags = "DOMAINS" }) },
|
|
-- [MobaXterm 스타일] 저장된 세션 바로가기 목록 (config.launch_menu, 퍼지 검색 가능)
|
|
{ key = "e", mods = "LEADER", action = act.ShowLauncherArgs({ flags = "FUZZY|LAUNCH_MENU_ITEMS" }) },
|
|
-- [MobaXterm 스타일] 탭/도메인/워크스페이스/launch_menu 전부 합친 통합 런처
|
|
{ key = "Tab", mods = "LEADER", action = act.ShowLauncherArgs({ flags = "FUZZY|TABS|DOMAINS|WORKSPACES|LAUNCH_MENU_ITEMS" }) },
|
|
|
|
-- [3] 검색 모드: leader+/ 로 스크롤백 문자열 검색
|
|
{ key = "/", mods = "LEADER", action = act.Search({ CaseInSensitiveString = "" }) },
|
|
-- [3] Copy mode: leader+[ 로 진입, vim 키(hjkl/v/y)로 스크롤백 탐색 & 선택
|
|
{ key = "[", mods = "LEADER", action = act.ActivateCopyMode },
|
|
-- [3] Quick select: leader+space 로 화면에 보이는 URL/해시/경로 등을 정규식으로 잡아 즉시 복사
|
|
{ key = "Space", mods = "LEADER", action = act.QuickSelect },
|
|
|
|
-- [6] Resize 모드 진입: leader+r 누른 뒤 모드 안에서 hjkl로 계속 크기 조절, Esc로 빠져나감
|
|
{ key = "r", mods = "LEADER", action = act.ActivateKeyTable({ name = "resize_pane", one_shot = false }) },
|
|
}
|
|
|
|
-- [6] key_tables: resize 모드처럼 진입 후 여러 번 연속 입력 가능한 모달 키바인딩
|
|
config.key_tables = {
|
|
resize_pane = {
|
|
{ key = "h", action = act.AdjustPaneSize({ "Left", 3 }) },
|
|
{ key = "l", action = act.AdjustPaneSize({ "Right", 3 }) },
|
|
{ key = "k", action = act.AdjustPaneSize({ "Up", 3 }) },
|
|
{ key = "j", action = act.AdjustPaneSize({ "Down", 3 }) },
|
|
{ key = "Escape", action = "PopKeyTable" },
|
|
{ key = "q", action = "PopKeyTable" },
|
|
},
|
|
}
|
|
|
|
-- [6] 마우스 바인딩: Ctrl+Shift+우클릭으로 붙여넣기, Ctrl+클릭으로 링크 열기
|
|
config.mouse_bindings = {
|
|
{
|
|
event = { Down = { streak = 1, button = "Right" } },
|
|
mods = "CTRL|SHIFT",
|
|
action = act.PasteFrom("Clipboard"),
|
|
},
|
|
{
|
|
-- ALT+클릭은 wezterm 기본 사각형(블록) 선택 제스처와 충돌해서 동작하지 않음
|
|
event = { Up = { streak = 1, button = "Left" } },
|
|
mods = "CTRL|SHIFT",
|
|
action = act.EmitEvent("show-main-menu"),
|
|
},
|
|
{
|
|
event = { Up = { streak = 1, button = "Left" } },
|
|
mods = "CTRL",
|
|
action = act.OpenLinkAtMouseCursor,
|
|
},
|
|
}
|
|
|
|
-- [3] Quick select 패턴 추가 (기본 URL/git hash 외에 IP주소, K8s pod명 등 추가 가능)
|
|
config.quick_select_patterns = {
|
|
"\\b(?:25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])\\.(?:25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])\\.(?:25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])\\.(?:25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])\\b", -- IPv4
|
|
}
|
|
|
|
-- ============================================================
|
|
-- [4] 하이퍼링크 / 이미지
|
|
-- ============================================================
|
|
-- 기본 URL 규칙 + 커스텀 규칙 추가
|
|
config.hyperlink_rules = wezterm.default_hyperlink_rules()
|
|
-- JIRA-1234 같은 티켓 링크가 필요하면 실제 도메인으로 바꾼 뒤 활성화하세요.
|
|
-- table.insert(config.hyperlink_rules, {
|
|
-- regex = [[\b[A-Z]{2,}-\d+\b]],
|
|
-- format = "https://jira.example.com/browse/$0",
|
|
-- })
|
|
|
|
-- Kitty graphics / iTerm2 / Sixel 이미지 프로토콜 활성화 (chafa, catimg 등으로 터미널 이미지 출력 가능)
|
|
config.enable_kitty_graphics = true
|
|
|
|
-- ============================================================
|
|
-- [5] 쉘 연동
|
|
-- ============================================================
|
|
-- 기본 실행 쉘/프로그램 (미지정 시 OS 기본 쉘)
|
|
-- config.default_prog = { "/bin/zsh", "-l" }
|
|
|
|
-- OSC 7으로 현재 디렉토리를 추적하려면 쉘 설정(.zshrc/.bashrc)에 shell integration 스크립트 추가 필요:
|
|
-- zsh: eval "$(wezterm shell-integration --shell zsh)" (또는 wezterm 문서의 zshrc 스니펫)
|
|
-- 적용되면 새 탭/분할이 현재 pane과 같은 디렉토리에서 시작됨
|
|
config.default_cwd = wezterm.home_dir
|
|
|
|
-- [MobaXterm 스타일] 저장된 세션 목록: leader+e (또는 우클릭 메뉴)로 목록을 띄워
|
|
-- 원하는 서버/커맨드를 바로 골라 새 탭으로 실행. 항목은 필요한 만큼 추가하면 됨.
|
|
-- (아래는 예시이며 실제 서버 정보로 교체 필요)
|
|
config.launch_menu = {
|
|
{ label = "Local bash", args = { "bash", "-l" } },
|
|
{ label = "htop", args = { "htop" } },
|
|
-- { label = "SSH: production", args = { "ssh", "user@production.example.com" } },
|
|
-- { label = "SSH: devbox", args = { "ssh", "user@devbox.example.com" } },
|
|
-- { label = "SSH: root custom port", args = { "ssh", "-p", "2222", "root@host.example.com" } },
|
|
}
|
|
|
|
-- ============================================================
|
|
-- [7] 렌더링 / 성능
|
|
-- ============================================================
|
|
config.front_end = "WebGpu" -- GPU 가속 렌더러 (문제 있으면 "OpenGL" 또는 "Software"로 변경)
|
|
config.webgpu_power_preference = "HighPerformance"
|
|
config.max_fps = 120
|
|
config.animation_fps = 60
|
|
|
|
-- 리가처(ligature) 비활성화 (=> 같은 코드 기호가 하나로 합쳐지는 게 싫으면 끄기)
|
|
config.harfbuzz_features = { "calt=0", "clig=0", "liga=0" }
|
|
|
|
-- ============================================================
|
|
-- 기타
|
|
-- ============================================================
|
|
config.scrollback_lines = 10000
|
|
config.enable_scroll_bar = false
|
|
config.audible_bell = "Disabled"
|
|
config.default_cursor_style = "BlinkingBar"
|
|
config.window_close_confirmation = "AlwaysPrompt"
|
|
|
|
return config
|