Dotfiles cho macOS: Từ Terminal đến Desktop Environment

Dotfiles cho macOS: Từ Terminal đến Desktop Environment

Phần 2: Cách AeroSpace, SketchyBar, tooling viết bằng Nushell, một Neovim IDE riêng, và hơn 80 công cụ CLI biến macOS thành một buồng lái dev điều khiển toàn bằng bàn phím.


Mục lục

Đây là Phần 2 trong series hai phần về dotfiles của tôi. Phần 1 — Dotfiles Của Tôi Không Phải Để Khoe. Chúng Để Làm Việc. lo phần móng: stow cho symlink, setup zsh + Starship, alias, Tmux, mấy lan can Git, và triết lý lười-nhưng-có-kỷ-luật. Bài đó trả lời một câu: làm sao để terminal của tôi vào việc được?

Bài này trả lời câu kế tiếp: còn lại đống cửa sổ khác thì tính sao?

Terminal mới chỉ là một cửa sổ. Còn cả tá cửa sổ khác — trình duyệt, editor, Slack, Docker Desktop, ba cái terminal khác nhau? Quản lý cửa sổ mặc định của macOS là một màn kịch kéo-thả-căn-chỉnh. Stage Manager trông đẹp trong keynote, nhưng bản chất vẫn là thủ công. Với một workflow điều khiển bằng bàn phím, đó là ma sát.

Đây là Phần 2: mọi thứ phía trên shell. Tiling cửa sổ, một thanh menu viết tay, tooling hạ tầng bằng Nushell, một Neovim IDE đầy đủ, và hơn 80 công cụ CLI đã thay thế đám mặc định — tất cả trong cùng một repo dotfiles, tất cả dựng lại được bằng đúng stow ..

AeroSpace: Tiling Cửa Sổ Trên macOS

Nếu bạn từng dùng i3 hay Sway trên Linux, bạn biết cảm giác kỳ diệu của một tiling WM. Cửa sổ tự snap vào các vị trí có thể đoán trước, dựa trên một cây logic — không kéo, không sắp xếp tay. AeroSpace mang triết lý đó về macOS.

Sức mạnh thật sự không nằm ở việc tile. Nó nằm ở các rule on-window-detected — tự động đẩy app vào đúng workspace của nó. Não tôi không còn phải nhớ cửa sổ đang ở đâu nữa. Chúng luôn ở chỗ cũ.

  • Workspace 2 — trình duyệt.
  • Workspace 3 — code (Warp, Neovim, GitHub Desktop).
  • Workspace 4 — chat (Slack, Discord).
View diagram source
graph LR
  Chrome["🌐 Chrome"] --> W2["Workspace 2
Browser"]
  Warp["💻 Warp"] --> W3["Workspace 3
Code"]
  Neovim["📝 Neovim"] --> W3
  GH["🐙 GitHub Desktop"] --> W3
  Slack["💬 Slack"] --> W4["Workspace 4
Comms"]
  Discord["🎮 Discord"] --> W4
# ~/.config/aerospace/aerospace.toml

[[on-window-detected]]
if.app-id = 'com.google.Chrome'
run = ['move-node-to-workspace 2']
[[on-window-detected]]
if.app-id = 'dev.warp.Warp-Stable'
run = ['move-node-to-workspace 3']
[[on-window-detected]]
if.app-id = 'com.tinyspeck.slackmacgap'
run = ['move-node-to-workspace 4']

# This triggers SketchyBar to update on every workspace switch
exec-on-workspace-change = [
    '/bin/bash', '-c',
    '~/homebrew/opt/sketchybar/bin/sketchybar --trigger aerospace_workspace_change'
]

Cái bẫy: ngồi script cho mọi app ngay ngày đầu. Tôi bắt đầu với đúng một rule — trình duyệt vào Workspace 2. Sống với nó một tuần. Chỉ thêm rule mới khi có app thực sự gây khó chịu. Hãy để workflow của bạn nói cho bạn biết cần tự động hoá cái gì.

SketchyBar: HUD Riêng Tự Build

AeroSpace là vô hình — chỉ có logic. Bấm chuyển workspace thì màn hình đổi, nhưng không có chỉ báo nào cho bạn biết đang ở đâu. Mất phương hướng cực nhanh.

SketchyBar thay luôn thanh menu mặc định của macOS bằng một thanh có thể script hoàn toàn. Nếu AeroSpace là cái não, SketchyBar là cái dashboard. Trạng thái của nó được điều khiển hoàn toàn bằng script shell, nên nó có thể phản ứng với bất kỳ thứ gì — kể cả event do AeroSpace bắn ra.

Mảnh ghép quan trọng: AeroSpace bắn event mỗi lần đổi workspace. SketchyBar nghe event đó và làm sáng workspace đang active. Chúng tạo thành một vòng phản hồi khép kín — window manager điều khiển layout, menu bar phản ánh layout đó ngay lập tức.

View diagram source
graph TB
  A["AeroSpace
Chuyển Workspace"] -->|"exec-on-workspace-change"| B["SketchyBar
Event Trigger"]
  B --> C["Cập Nhật Chỉ Báo
Workspace"]
  C --> D["Highlight App
Đang Focus"]
  D -->|"Phản hồi trực quan"| E["Bạn Luôn Biết
Mình Đang Ở Đâu"]

SketchyBar của tôi hiện workspace đang active, app đang focus, nhạc, và trạng thái VPN. Tất cả dùng một bảng màu “Ultra Rich” tự đặt — đen sâu, neon đậm, đồng bộ với theme của các terminal.

# ~/.config/sketchybar/plugins/aerospace_display.sh (simplified)

FOCUSED_WORKSPACE=$(aerospace list-workspaces --focused)
MONITOR_M1_WORKSPACE=$(aerospace list-workspaces --monitor 1 | head -n1)

if [ "$MONITOR_M1_WORKSPACE" = "$FOCUSED_WORKSPACE" ]; then
    sketchybar --set M1_display icon.color=$WHITE background.color=$HIGHLIGHT
else
    sketchybar --set M1_display icon.color=$GREY background.color=$TRANSPARENT
fi

Cái bẫy: quá tải thông tin. Bản đầu tiên của tôi hiện CPU, RAM, ổ đĩa, mạng, và thời tiết. Trông ngầu, nhưng là nhiễu. Tôi cắt sạch, chỉ giữ lại đúng cái thiết yếu: workspace nào, app nào đang focus. Mọi thứ khác đều phải tự kiếm chỗ đứng của nó.

Ba Terminal, Ba Vai Trò

Dotfiles của tôi cấu hình ba terminal. Không phải vì tôi không quyết được — mà vì mỗi cái phục vụ một mục đích khác nhau.

Kitty — Terminal Cho Lúc Cần Tập Trung

Tăng tốc GPU, tuỳ chỉnh vô hạn. Daily driver cho những phiên code dài. Nền blur, trong suốt, kèm hình nền riêng tạo cảm giác “trạm làm việc cyberpunk” — và bất ngờ là rất dễ chịu mắt khi nhìn liên tục nhiều giờ.

# ~/.config/kitty/kitty.conf

font_family      JetbrainsMono Nerd Font
font_size 11.5
background_opacity 0.90
background_blur 90
background_image ~/dotfiles/kitty/themes/bg.jpg
background_tint 0.9
include themes/Catppuccin-Mocha.conf

Ghostty — Terminal Để Khoe Shader GPU

Tính năng sát thủ của Ghostty: shader GPU tự viết, áp thẳng vào pipeline rendering. Tôi chồng ba cái — bloom cho text sáng, một cursor blaze kiểu tương lai, và một vệt mờ nhẹ theo cursor. Hoàn toàn không cần thiết. Nhưng nó cho cảm giác như đang code trong tương lai.

# ~/.config/ghostty/config

custom-shader = shaders/bloom025.glsl
custom-shader = shaders/cursor_blaze_no_trail.glsl
custom-shader = shaders/cursor_smear.glsl

Warp — Terminal Cho Lúc Làm Việc Nhóm

Block-based, có AI hỗ trợ, build cho việc cộng tác. Tôi mở nó khi pair-debug, hoặc khi cần Warp AI giải nghĩa một stack trace khó nhằn. Dotfiles của tôi giữ cho theme của nó đồng bộ với phần còn lại của hệ thống.

Quy tắc: chỉnh effect đến khi cảm thấy nhiều hơn nhìn thấy. Bloom mạnh hay blur dày làm chữ khó đọc. Mục tiêu là không khí, không phải sàn nhảy.

Nushell: Cái Shell Tư Duy Bằng Bảng

Đây là chỗ tôi đi xa hơn Phần 1. .zshrc lo phần shell hằng ngày. Nhưng với các tác vụ hạ tầng — kết nối database, nhảy vào pod Kubernetes, SSH vào EC2 — tôi dùng Nushell. Khác biệt thực sự sâu.

Nushell coi mọi thứ là dữ liệu có cấu trúc. Không phải chuỗi. Không phải đống text để grep. Mà là dữ liệu có kiểu, có cột, có hàng. Khi điều hướng một loạt database hay pod, bạn không parse mấy bức tường text — bạn query bảng.

Tôi đã build một bộ lệnh Nushell riêng, biến cả hạ tầng thành một TUI chạy bằng fzf:

  • go2pod — duyệt K8s context, namespace, và pod. Chọn một, chọn container, drop thẳng vào shell. Khỏi phải thuộc lòng tên namespace hay copy-paste pod hash.
  • go2db — liệt kê toàn bộ database StrongDM (MySQL, Aurora, DocumentDB, Redis), fuzzy-select, tự kết nối, mở đúng client. Một lệnh.
  • go2ssh — flow tương tự cho SSH server. Chọn, kết nối, xong.
  • go2ec2 — duyệt EC2 instance đang chạy kèm trạng thái SSM. Kết nối qua Session Manager. Khỏi cần SSH key.
# go2db — one command to reach any database in the fleet

export def go2db [--fresh] {
    let databases = if not $fresh {
        let cached = (read-cache "db" "databases")
        if ($cached != null) { $cached } else {
            let fresh_data = (dblist)
            write-cache "db" "databases" $fresh_data
            $fresh_data
        }
    } else { dblist }

    let sorted = (sort-by-access "sdm" $databases)
    let selected = (fzf-select $sorted "Select database:")
    connect-by-type $selected  # Auto-detects MySQL vs Mongo vs Redis
}

Mỗi lệnh đều có cache 24 giờ, sắp xếp theo lịch sử truy cập (resource hay dùng tự nổi lên đầu), và một bảng màu Catppuccin Mocha xuyên suốt. Error handling là thật chứ không qua loa — nếu StrongDM chưa auth, lệnh bắt lỗi và tự khởi động lại flow đăng nhập.

Vũ khí thật sự nằm ở pipeline có cấu trúc của Nushell. Tô màu trạng thái pod là một biểu thức match, không phải regex. Sắp xếp theo lần dùng cuối là sort-by trên một cột datetime thật, không phải mấy chiêu so chuỗi. Đây là khác biệt giữa scripting và programming.

Hạn chế thật: module system của Nushell yêu cầu một số import phải là hằng số tại parse-time. Bạn không thể overlay use một đường dẫn virtualenv lưu trong biến một cách động. Tôi có cách workaround, nhưng nó vẫn là một vết xước. Đổi lại vẫn đáng.

Neovim: Một Django IDE Nằm Gọn Trong Repo Dotfiles

Config Neovim của tôi bắt đầu từ Kickstart.nvim và lớn lên thành một Python/Django IDE đầy đủ. Cấu trúc hai lớp: core/ cho các plugin ổn định cấp framework, custom/ cho mấy tinh chỉnh theo workflow cá nhân.

Kiến Trúc Plugin

Stack có chính kiến rõ ràng:

  • blink.cmp cho completion (nhanh hơn nvim-cmp, fuzzy matching native)
  • Mason + mason-lspconfig để tự quản lý LSP server
  • Telescope để fuzzy find mọi thứ — file, symbol, diagnostic, help doc
  • Treesitter cho cú pháp, conform cho format, nvim-lint cho lint
  • lazy.nvim để quản lý plugin và lazy-load

Keybinding Hiểu Django

Module custom/django.lua tự phát hiện project Django (kiểm tra manage.py) rồi load nguyên một bộ keybinding:

-- Auto-loaded when manage.py is detected in the project root

vim.keymap.set('n', '<leader>dr', function()
    run_django_command('runserver')
end, { desc = 'Django: Run server' })

vim.keymap.set('n', '<leader>dmm', function()
    run_django_command('makemigrations')
end, { desc = 'Django: Make migrations' })

vim.keymap.set('n', '<leader>dfm', function()
    find_django_file('models.py')
end, { desc = 'Django: Find models.py' })

<leader>dr chạy dev server. <leader>dmm tạo migration. <leader>dfm nhảy tới bất kỳ models.py nào qua Telescope. Bộ đầy đủ phủ view, URL, setting, admin, form, template, và test. Không phải plugin — chỉ là 90 dòng Lua, và nó biến mất hoàn toàn khi bạn không ở trong project Django.

AI — Ba Lớp Tách Bạch

Config có ba công cụ AI, mỗi công cụ cho một mục đích khác nhau:

  • Copilot — gợi ý ghost-text inline khi đang gõ. Luôn bật, không làm phiền.
  • Claude Code — một terminal nổi chạy Claude như một coding agent hoàn chỉnh. Ctrl-, để toggle, <leader>cc để tiếp tục cuộc hội thoại.
  • CodeCompanion — chat backed bởi Ollama, cho các query AI chạy local và private. Mặc định dùng Qwen 3. Có tích hợp MCP Hub để gọi tool.

Lớp Thẩm Mỹ

Theme GitHub Dark Default với nền trong suốt. Statusbar Lualine dùng tone Catppuccin Mocha — xanh dương cho normal, xanh lá cho insert, sapphire cho visual, kèm một spinner riêng cho tiến trình LSP. Snacks.nvim lo notification, indent guide, và scroll mượt. Dashboard hiện project và file gần đây kèm vài câu quote ngẫu nhiên.

Hơn 40 plugin, nhưng đều lazy-load. Startup vẫn dưới 80ms.

Kho Đồ Nghề CLI

Brewfile của tôi có hơn 80 formula. Đây là những thứ đã thực sự đổi cách tôi làm việc:

Công cụ cũThay bằngVì sao đáng đổi
lsezaTree view, hiện git status, icon, hyperlink
cdzoxideNhảy theo “frecency”. z proj ăn đứt cd ~/Documents/projects/foo
historyatuinShell history lưu SQLite, đồng bộ giữa các máy, search được
findfdDefault hợp lý, tôn trọng .gitignore, nhanh gấp 5
grepripgrepCùng câu chuyện — nhanh hơn, default thông minh hơn
catbatSyntax highlight, tích hợp git, số dòng
Tab completioncarapaceMột bộ completion cho nhiều shell: zsh, fish, bash, Nushell
topbtopResource monitor đẹp, có hỗ trợ chuột
Docker UIlazydockerTUI quản lý Docker đầy đủ
Git UIlazygitStage, rebase, giải quyết conflict — tất cả interactive

Và còn oh-my-posh — engine prompt đa shell với config JSON tự viết. Prompt của tôi hiện thư mục hiện tại, Git branch kèm số commit ahead/behind (dùng emoji: xe đua cho ahead, con rùa cho behind), phiên bản Python, thời gian thực thi cho các lệnh chậm, và một icon xương rồng khi lệnh thành công. Cùng một prompt dù tôi đang ở Zsh, Nushell, hay SSH từ xa.

Toàn bộ setup — mọi tap, formula, cask — đều nằm trong một Brewfile. Một máy Mac trắng tinh sẽ về đúng môi trường đầy đủ chỉ với:

brew bundle --file ./homebrew/Brewfile
stow .

Hết. Không bước tay nào. Không “à, quên cài X”. Brewfile chính là tài liệu.

Triết Lý

Một repo dotfiles không phải bản backup. Nó là bản đặc tả cách bạn tương tác với cái máy của mình. Mỗi file trong đó là một quyết định đã được đóng gói: terminal này cho việc này. Workspace này cho app này. Keybinding này vì tay tôi đã mặc định nó nằm ở đó.

Sai lầm phổ biến là coi dotfiles như một bộ sưu tập config. Không phải. Nó là một hệ thống. AeroSpace định tuyến cửa sổ. SketchyBar phản ánh đúng cái định tuyến đó. Nushell cấu trúc lại đường vào hạ tầng. Neovim đóng vai trò lõi chỉnh sửa. Đám CLI lấp những khoảng trống còn lại. Và stow cột tất cả lại với nhau, không cần ai quản lý symlink bằng tay.

Hãy bắt đầu với một mảnh. Làm cho nó đúng. Rồi thêm mảnh kế tiếp.

Và đây là khi tất cả các mảnh ghép vào đúng vị trí — một screenshot, mọi lớp đều đang vận hành:

Toàn bộ môi trường dotfiles — AeroSpace tile cửa sổ, SketchyBar HUD, các terminal có theme, và Neovim IDE — tất cả điều khiển bằng cùng một repo

Ngừng cấu hình app. Bắt đầu thiết kế môi trường của bạn.

Nếu bạn lỡ phần móng nằm dưới tất cả những thứ trên — shell, alias, kỷ luật Tmux, mấy lan can Git — đó là Phần 1: Dotfiles Của Tôi Không Phải Để Khoe. Chúng Để Làm Việc. Hai phần ghép lại thành cả stack: từ terminal lên, từ desktop xuống, đúng một repo.

Luồng

0
⌘/Ctrl+Enter để gửiGõ / để xem lệnh · Tab để @nhắc tên