Small changes; cleanup, unsuccessful map
[lyqi-vim.git] / lyqi-all.vim
blob83305f4cef7fd7172622ce3aa71f74dd9f2c85a8
1 " lyqi.vim
2 " @Author:      Eyolf Østrem (mailto:eyolf curlie oestrem small com)
3 " @Website:     http://oestrem.com
4 " @License:     GPL (see http://www.gnu.org/licenses/gpl.txt)
5 " @Created:     14-May-2008.
6 " @Last Change: Mon Nov 17 20:32:32 CET 2008
7 " @Revision:    0.0.1
8 " TODO:  {{{1
9     " macros for \ficta, \fermata, " \times, etc
10     " maps/plugin architecture
11     " customizable, user-defined layout and macros
12     " templates
13     " fix the spacing bugs that are still left (why won't pitch() enter a
14     " space before the note when the cursor is on the end of the line?)
15     
16 "======================================================================
17 " USAGE: {{{1
18 "======================================================================
19 " The script simplifies note entry for lilypond files. Three different
20 " kinds of tasks are performed with single or just-a-few key presses: 
21 " - entry of a new note; 
22 " - modification of an existing note (wrt duration, accidentals, octave,
23 "   dots, cautionary accidentals, and articulation signs); 
24 " - certain special signs, such as fermata, musica ficta, \times x/y {}, etc.
26 " The keyboard is completely remapped: the left hand enters the pitches, in
27 " the sequence of a piano keyboard, and the right hand 'plays' the rhythms,
28 " which are laid out 'ergonomically' from the \breve (B) to the 32nd note (P):
29 " 64th and 128th notes re-use the O and P keys in shifted position, and
30 " \longa and \maxima are placed on <S-l> and <S-m>. 
31 " Flats and sharps are added with 'c' and 'v', octaves are modified with
32 " 'i' (up) and 'm' (down), and cautionary accidentals  are entered with '!'
33 " and '?'. A \fermata is added with '.'
35 " -------------------------------------------------------------------------  
36 " |  s  |  g  |  a  |  b  |times|     |     |  '  |16/64|32/128     |     |
37 " |  Q  |  W  |  E  |  R  |  T  |  Y  |  U  |  I  |  O  |  P  |     |     |
38 " ---------------------------------------------------------------------------  
39 "   |  c  |  d  |  e  |  f  | r/R |  1  |  2  |  4  |  8  |     |     |     |
40 "   |  A  |  S  |  D  |  F  |  G  |  H  |  J  |  K  |  L  |     |     |     |
41 "   ------------------------------------------------------------------------- 
42 "     |undo | del |flat |sharp|breve| dot |  ,  |     |     |     |     |
43 "     |  Z  |  X  |  C  |  V  |  B  |  N  |  M  |     |     |     |     |
44 "     -------------------------------------------------------------------
46 " The home row is used for the most common elements. 
47 " The layout ensures that values that are likely to be close together
48 " (stepwise motion and leaps of fourths; 'f' + 'sharp', 'e' + 'flat';
49 " adjacent rhythm values, etc.) are close together also on the keyboard. 
51 " Any of the "pitch keys" (asdfwer, plus qgG for s, r, and R) enters a
52 " single note name. Accidental modifications are rememebered, so one
53 " doesn't have to change every 'f' to 'fis' in g major. Modifications of
54 " the simple note is done subsequently. E.g., to turn  
56 "                  f     into      fisis!,\breve..
57
58 " one would type the keys 'vv!mbnn' in any order.
59
60 "The mode is initialized on startup, or with the vim command Lyqi_init(). To
61 "enter music, run the function Lyqi_key(), which is an infinite loop (exit
62 "with <C-c>). 
64 "The arrow keys navigate between the note strings, and 'z' is mapped to
65 "'undo'.
67 "======================================================================
68 "   Initialization {{{1
69 "======================================================================
70 if exists("g:loaded_Lyqi")
71     delfun Lyqi_init
72     delfun Lyqi_key
73     delfun Get_current_note
74 endif
76 let g:loaded_Lyqi = 1 " your version number
80 fun! Lyqi_init()
81     " initializes the values that are used throughout. All are immutable
82     " and are supposed to be so, except pitchmap, which will change
83     " according to the use of accidentals in the course of a piece.
84 py << EOF
85 import re 
86 import math
87 import vim
89 global pitches, pitch_keys, pitchmap
90 pitches = ( "c", "d", "e", "f", "g", "a", "b", "s", "r", "R" )
91 pitch_keys = ( "a", "s", "d", "f", "w", "e", "r", "q", "g", "G" ) 
92 pitchmap = dict(zip(pitch_keys, pitches))
93 accs = ( -1, 1 )
94 acc_keys = ( "c", "v" )
95 accmap = dict(zip(acc_keys, accs))
96 cauts = ( "!", "?" )
97 caut_keys = ( "!", "?" )
98 cautmap = dict(zip(caut_keys, cauts))
99 octs = ( -1, 1 )
100 oct_keys = ( "m", "i" )
101 octmap = dict(zip(oct_keys, octs))
102 durs = ( "128", "64", "32", "16", "8", "4", "2", "1", "\\breve", "\\longa", "\\maxima" )
103 dur_keys = ( "P", "O", "p", "o", "l", "k", "j", "h", "b", "L", "M" )
104 durmap = dict(zip(dur_keys, durs))
105 dots = ( "." )
106 dot_keys = ( "n" )
107 valid_note = ("pitch", "acc", "caut", "oct", "dur", "dot", "art", "add")
108 current = { "pitch": "c", "acc": "", "caut": "", "oct": "", "dur": "", "dot": "" , "art": "", "add": "" }
109 new_note = ""
110 vim_note = ""
111 notestring = r"""^(?P<pitch>[a-grsR])
112 (?P<acc>(((ses)|(s))|((es){1,2})|((is){1,2}))?)
113 (?P<caut>[?!]*)
114 (?P<oct>[,']*)
115 (?P<dur>((16)|1|2|4|8|(32)|(64)|(\\breve)|(\\longa)|(\\maxima))?)
116 (?P<dot>[.]*)
117 (?P<art>([-_^\\].*)*)
118 (?P<add>.*)$"""
119 parsed_note = re.compile(notestring,  re.VERBOSE) 
121 # here begins the python code which does all the string processing and --
122 # eventually -- the midi output.
123 # Contains: 
124 # - a wrapper function, process_key(), which decides which function to call
125 #   depending on the input key, 
126 # - specialized functions for each of the note-string elemenst (pitch, acc,
127 #   caut, oct, dur, dot, art (for articulation signs etc.) and add
128 #   (whatever is left over...)
129 # - make_note(), which generates the new note string.
131 # TODO: add functions for art and add
133 # Python functions {{{1
134 #======================================================================
135                                 #Parse {{{2
136 #======================================================================
137 def parse(input_string):
138     match_obj = parsed_note.search(input_string)
139     for i in valid_note:
140         current[i] = match_obj.group(i)
141     #adjust the inconsistent accidental syntax
142     if current['acc'].startswith('s'):
143         current['acc'] = 'e' + current['acc']
146                                 #pitch {{{2
147 #======================================================================
148 # - change current['pitch'] 
149 # - TODO: play a sound according to cur_note['pitch'] and [oct]
151 def pitch(input_key):
152     current['pitch'] = pitchmap[input_key]
153     n = current['pitch']
154     if vim.eval("col('.')") == 1:
155         vim.command("normal i" + n)
156     elif vim.eval("col('$')-col('.')") == 1:
157         vim.command("normal a " + n)
158     else:
159         n += " " 
160         vim.command("normal a" + n)
161         #vim.command("normal a ")
163 #======================================================================
165                                 #acc {{{2
166 #======================================================================
167 def acc(input_key):
168     vim.command("call Get_current_note()")
169     global note
170     note = vim.eval("b:notestring")
171     parse(note)
172     #calculate the new value for acc -- up or down?
173     if 'e' in current['acc']:
174         esis = -1
175     else:
176         esis = 1
177     accnum = len(current['acc']) / 2 * esis + accmap[input_key]
178     if accnum < -1:
179         current['acc'] = 'eses'
180     elif accnum == -1:
181         current['acc'] = 'es'
182     elif accnum == 0:
183         current['acc'] = ''
184     elif accnum == 1:
185         current['acc'] = 'is'
186     else:
187         current['acc'] = 'isis'
188     for k in pitchmap:
189         if pitchmap[k][:1] == current['pitch']:
190             pitchmap[k] = current['pitch'] + current['acc']
191     vim.command("normal i" + make_note())
193 #======================================================================
194                                 #dur {{{2
195 #======================================================================
196 def dur(input_key):
197     vim.command("call Get_current_note()")
198     note = vim.eval("b:notestring")
199     parse(note)
200     current['dur'] = durmap[input_key]
201     current['dot'] = ''
202     vim.command("normal i" + make_note())
204 #======================================================================
205                        #cautionary accidentals {{{2
206 #======================================================================
207 def caut(input_key):
208     vim.command("call Get_current_note()")
209     note = vim.eval("b:notestring")
210     parse(note)
211     current['caut'] = cautmap[input_key]
212     vim.command("normal i" + make_note())
214 #======================================================================
215                             #octave signs {{{2
216 #======================================================================
217 def oct(input_key):
218     #get current note from vim and parse it into current{}
219     vim.command("call Get_current_note()")
220     note = vim.eval("b:notestring")
221     parse(note)
222     if ',' in current['oct']:
223         octdir = -1
224         octsign = ','
225     elif "'" in current['oct']:
226         octdir = 1
227         octsign = "'"
228     else: 
229         octdir = 0
230         if octmap[input_key] == -1:
231             octsign = ',' 
232         else:
233             octsign = "'"
234     octnum = abs(len(current['oct']) * octdir + octmap[input_key])
235     current['oct'] = octnum * octsign
236     vim.command("normal i" + make_note())
238 #======================================================================
239                                  #dot {{{2
240 #======================================================================
241 # TODO: make function for backwards scanning after rhythm value. In the
242 # meantime, a default value of 4 will have to do.
243 def dot():
244     vim.command("call Get_current_note()")
245     note = vim.eval("b:notestring")
246     parse(note)
247     if not current['dur']:
248         current['dur'] = '4'
249     current['dot'] += '.'
250     vim.command("normal i" + make_note())
253 #dur = siste_dur
254 #def find_prev_dur():
255 #    dur_search = []
256 #    for i in durs:
257 #        dur_search[i] = '\\(' + durs[i] + '\\)'
258 #    dur_str = '\\|'.join(dur_search)
259 #    dur_match = vim.command("call search("+search_str+", 'bcpn')")
260 #    current['dur'] = durs[dur_match-1]
262 #======================================================================
263                              #make_note {{{2
264 #======================================================================
265 def make_note():
266     new_note = ""
267     for i in valid_note:
268         new_note += current[i]
269     return new_note
271 #======================================================================
272                               #process_key(): {{{2
273 #======================================================================
274 def process_key():
275     key = vim.eval("b:input_key")
276     if key in pitch_keys:
277         pitch(key)
278     elif key in acc_keys:
279         acc(key)
280     elif key in oct_keys:
281         oct(key)
282     elif key in caut_keys:
283         caut(key)
284     elif key in dur_keys:
285         dur(key)
286     elif key in dot_keys:
287         dot()
288     else:
289         vim.command("normal a " + key)
291 endfun
292 " }}}2
293 call Lyqi_init()
295 "Vim functions {{{1
296 "======================================================================
297                     "function! Get_current_note() {{{2
298 "======================================================================
299 " collects the note-string prior to the cursor position (here only a
300 " rudimentary check is done: the first string which begins with one of
301 " the note-characters)
302 " So far limited to plain strings; chords will have to come at a later
303 " stage.
304 function! Get_current_note() 
305     call search('\<[a-gRrs]', 'bc')
306     "let save_cursor = getpos(".") 
307     execute "normal diW" 
308     let b:notestring = getreg('"') 
309     "call setpos('.', save_cursor) 
310 endfunction
311 "======================================================================
312                               "Lyqi_key {{{2
313 "======================================================================
314 function! Lyqi_key()
315     2match Error /\<\S\{-}\%#\S\{-}\>\|^\%#\s*/
316     match WarningMsg /\%#/
317     let b:input_key = 1
318     while b:input_key != "å" 
319         "positions the cursor at current or following whitespace. Doesn't
320         "capture repeated whitespace, but never mind... can be cleaned up with
321         "a general function 
322         "call cursor(".", searchpos('\_s', 'ce')[1])
323         call search('\_s', 'c')
324         redraw
325         "input key press
326         " navigation keys; interpreted directly
327         let b:input_key = getchar()
328         if b:input_key == "\<Left>"
329             call search('\_s', 'b')
330             redraw
331             match Error /\<\S\{-}\%#\S\{-}\>\|^\%#\s*/
332             continue
333         elseif b:input_key == "\<Right>"
334             call search('\_s', '')
335             redraw
336             match Error /\<\S\{-}\%#\S\{-}\>\|^\%#\s*/
337             continue
338         elseif b:input_key == "\<Down>"
339             normal j
340             call search('\_s', '')
341             redraw
342             match Error /\<\S\{-}\%#\S\{-}\>\|^\%#\s*/
343             continue
344         elseif b:input_key == "\<Up>"
345             normal k
346             call search('\_s', '')
347             redraw
348             match Error /\<\S\{-}\%#\S\{-}\>\|^\%#\s*/
349             continue
350         else
351             " character keys; interpreted after conversion to char
352             let b:input_key = nr2char(b:input_key)
353             if b:input_key == 't'
354                 exe "normal a\\times " . input("Fraction: ", "2/3") . " { " 
355                 redraw
356                 continue
357                 if b:input_key == '}'
358                     exe "normal i} "
359                     redraw
360                     continue
361                 endif
362             elseif b:input_key == '.'
363                 exe "normal a\fermata "
364                 redraw
365                 continue
366             elseif b:input_key == '\'
367                 exe "normal a\\" . input("Escaped sequence: ") . " "
368                 redraw
369                 continue
370             elseif b:input_key == 'x'
371                 call Get_current_note()
372                 let line = getline('.')
373                 substitute(eval(line), " \+", " ", "g")
374                 redraw
375                 continue
376             elseif b:input_key == 'z'
377                 normal u
378                 redraw
379                 continue
380             elseif b:input_key == 'Z'
381                 redo
382                 redraw
383                 continue
384             else
385                 python process_key()
386                 redraw
387             endif
388             redraw
389         endif
390         redraw
391     endwhile
392     match
393 endfunction 
395 match
397 command! LyqiMode :call Lyqi_key()
398 noremap <f2> LyqiMode<cr>
401 " vim:fdm=marker