컬러스킴을 전환하기

vim에서 컬러 스킴을 변경하고 싶을 때에 :colorscheme 명령을 사용한다. (줄여서 :color로 쓸 수 있다.) 이 명령으로 컬러 스킴을 변경할 때에는 테마 이름을 알아야 하는데, 사실 몰라도 상관없다. :color<space> 한 후에 탭 키를 누르면 테마 이름은 자동완성되기 때문이다. 근데 이것보다도 키 하나만 눌러서 다른 테마로 적용되는 것을 보면서 전환하도록 하는 것이 더 편리할 것 같다. 이 기능을 vim 안에서 어떻게 만들면 좋을지 알아보자.

전체 컬러 목록 만들기

컬러 스킴을 전환하려면 사용 가능한 전체 컬러의 이름 목록이 있어야 할 것이다. 이 목록을 만드는 작업부터 시작해보자.

let s:colorNameList = []

func! s:GetThemeNames()
  let s:colorNameList = []
  let s:paths = split(glob("$VIMRUNTIME/colors/*.vim"), '\n')
  for s:path in s:paths
    let s:name = fnamemodify(s:path, ":r:t")
    call add(s:colorNameList, s:name)
  endfor
endfunc

s:colorNameList는 컬러스킴의 이름을 담아두는 리스트이다. 기본적인 컬러테마는 vim 설치위치 내에 colors 디렉토리 아래에 vim 파일로 정리되어 있다. glob() 함수를 사용해서 이 파일들의 경로를 얻은 다음, 개행문자로 분리하여 파일 경로의 리스트를 얻는다. 각각의 경로는 fnamemodify() 함수에 ":r:t" 옵션을 사용하여 확장자를 제외한 파일의 이름만 추출한다. 이 때, :t 옵션은 경로의 최하위 컴포넌트 (이름.확장자)를 의미하며, :r 옵션은 확장자를 제외하는 것을 말한다.

이렇게 얻은 이름들을 하나씩 s:colorNameList에 추가한다. 리스트에 추가하는 함수는 add() 인데, 명령줄 상태에서 함수만 호출해야하므로 call 명령을 쓰고 있음에 유의하자.


현재 스킴 파악하기

다음 컬러스킴으로 변경하기 위해서는 현재 컬러스킴이 이름 목록에서 몇 번째인지를 알아내야 한다. 이 동작을 s:GetCurrentThemeIndex()함수에서 정의하고 있다. 현재 컬러 스킴의 이름은 :colorscheme 명령을 실행하여 알 수 있는데, g:colors_name에도 이 값이 기록되어 있기 때문에 그대로 사용하면 되겠다.

func s:GetCurrentThemeIndex()
  let s:name = g:colors_name
  return index(s:colorNameList, s:name)
endfunc

최종적으로 index() 함수를 사용해서 리스트에서 몇 번째 이름의 컬러스킴을 사용하는지 알아낼 수 있고 이 값을 리턴하면 된다.


컬러 변경 동작 구현하기

이번엔 컬러스킴을 변경하는 함수를 작성할 차례이다. 여기서도 역시 :colorscheme 명령을 사용할 것이다. 주의할 점은 :colorscheme 명령은 그 자체로도 Ex명령이지만, 이 경우에 컬러스킴 이름이 변수에 있기 때문에 colorscheme s:nname 이라고 하면 "s:nname"이라는 컬러를 사용하려고 시도하기 때문에 동작하지 않는다. 따라서 :execute 명령을 사용해서 컬러를 전환한다. 이 명령에서 인자로 받는 명령은 문자열 형태로 전달해주면 되기 때문에 변수의 내용을 사용할 수 있다.

이제 다음 번 컬러로의 변환하는 방법은 간단하다. 현재 컬러의 다음 인덱스를 구해서 해당 위치의 컬러 이름으로 ":colorscheme 다음색이름" 명령을 실행하면 된다.

s:ChangeToNexTheme() 함수가 이를 수행한다. 먼저 s:colorNameList 의 원소의 개수를 구해서 2개미만이면 s:GetThemeList()를 호출해서 리스트를 구성한다. 그리고 다음 인덱스를 계산한 후, 그에 해당하는 이름을 얻는다.

func! s:ChangeToNextTheme()
  if len(s:colorNameList) < 2
    call s:GetThemeNames()
  endif
  let s:cid = (s:GetCurrentThemeIndex() + 1) % len(s:colorNameList)
  let s:nname = s:colorNameList[s:cid]
  execute "colorscheme " . s:nname
endfunc

끝으로 s:ChangeToNextTheme() 함수를 실행해주는 명령을 등록하고, F10 키를 여기에 맵핑하자.

command! -nargs=0 ColorNext call s:ChangeToNextTheme()
nnoremap <F10> :ColorNext<cr>

함수, 명령 설명

이 글에서 사용한 함수나 명령에 대한 간략한 설명을 따로 정리했다. 자세한 내용은 vim의 :help 명령을 사용해서 확인하자.

  • glob({expr} [, {nosuf} [, {list} [, {alllinks}]]]) :
    {expr}에 정의된 파일 와일드 카드를 확장한다. 결과는 NL로 이어진 하나의 문자열이므로 split()을 통해 분리하여 써야 한다.
  • split({expr} [, {pattern} [, {keepemtpy}]]) :
    {expr} 을 분리하여 리스트를 만든다. 패턴이 생략되면 공백문자를 기준으로 자른다. {keepemtpy} 옵션은 빈 문자열을 리스트에 포함시킬 것인지 여부를 결정한다.

    :let words = split(getline('.'), '\W\+') 는 현재 줄에서 단어들로만 구성된 리스트를 만들어낸다.
  • fnamemodify({fname}, {mods}) :
    {fname}으로 주어진 파일 이름을 변형한다.
  • index({list}, {expr} [, {start} [, {ic}]]) :
    리스트에서 표현식 값이 등장하는 첫 인덱스를 구한다. 존재하지 않으면 -1이 리턴된다.
  • len({expr}) :
    길이를 구한다. 문자열, 리스트, 사전 등을 인자로 받을 수 있다.
  • :[range]call {name}([arguments]) :
    함수에 인자를 전달하여 호출한다. 범위가 주어지면 범위 내의 각 라인에 대해 반복실행한다. 만약 함수가 범위를 다룰 수 있다면 a:firstline, a:lastline 이 인자로 전달된다.
  • aList[idx] 와 같은 식으로 리스트에서 인자를 통해 값을 얻을 수 있다. 비슷한 동작으로 get({list}, {index})가 있다. (List 도움말 참조)
  • execute({command} [, {silent}]):
    문자열값을 받아 Ex명령으로 실행하고 그 결과를 문자열로 리턴한다. 리스트를 전달하는 경우, 명령이 하나씩 순차적으로 실행된다. {silent} 옵션을 주면 명령을 수행하는 동안 메시지를 출력하지 않는다. :silent:silent!로 줄 수 있다.
  • :execute {expr1} .. : Ex 명령을 실행한다.
  • :colorscheme [{name}] : 주어진 이름으로 컬러 스킴을 변경한다. 이름이 주어지지 않으면 현재 컬러스킴 이름을 표시한다. 현재 컬러스킴 이름은 g:colors_name 이다.

부록 2 – 파일이름 수정자

fnamemodify() 함수에서 사용할 수 있는 파일 이름 수정자는 아래와 같다. 이 수정자는 expand() 함수에서도 같이 사용할 수 있다.

  • :p – 파일 이름을 절대 경로로 변경
  • :8 – 파일 경로를 8.3 포맷으로 변경
  • :~ – 홈 디렉토리로부터의 상대경로로 변경
  • :. – 현재 디렉토리로부터의 상대경로로 변경
  • :h – 파일 이름 앞까지의 경로
  • :t – 경로의 마지막 컴포넌트
  • :r – 확장자를 없앤다.
  • :e – 확장자
  • :s?pat?sub – 처음 등장하는 pat을 sub로 치환한다.
  • :gs?pat?sub – 모든 pat을 sub로 치환한다.
  • :S – 쉘 명령에 적용할 수 있도록 특수 문자를 이스케이프처리한다.