Tuesday, January 22, 2019

Vim (for non-programmers) Chapter O (NOT 0), recipes which are quick and dirty, example six: Let's Make a Time-Stamped Log of Stuff We Read Online and Want to Have a List Of; Hey, Guess What? I Got a Lot of This from Chris Toomey (heart-eyes emoji)

(Or, more generally: having Vim do some stuff automagically and plus yet still even lots more fun with functions both built-in and homebrewed, goals probably often shared by many.)

So's anyways, I was thinking I might could benefit from a list of stuff I read online, and since sometimes I read online on a computer, I thought I could try to make the computer help me out with making that list. It only took about a billion hours to work out how. And, of course, what's a list of stuff you did, unless it's also a list of when you did it? Also that's a thing I would like the computer help me out with. And what good is a list if it's not, you know, nicely formatted and fun to read? Not much fucking good at all if you ask me, bud.

Full Disclosure: a big part of this project was that I had a potent itch to just ... learn some stuff, and do something new. The problem is that I am pretty terrible at learning in a vacuum*, so I needed to figure out a need that learning something could address. This was pushing up at the end of the year, so I was, amangst other things, going thru my lists, so I figured, "Hey, let's make MORE lists, that sounds fun". This collided with my occasional dicking around on over to YouTube, which led me to another Chris Toomey video that was as good as the one I saw that drove me to madness helped me make some easier ways to do stuff in Markup, which led me to a now-ancient blog post, which contained a magnificent little skeleton for making, testing, and building up / out a Vim function. So that's where I started.

*Can't breathe, for one.

Chris Toomey's function skeleton looks like so: you put it in a file someplace, and save the file:
function! DoSomething()
    echo 'Hi, Fat.'
endfunction

nnoremap <leader>tf :source %<cr>:call DoSomething()

So. The first line says "define a function named DoSomething()", which is maybe a little embarrassingly basic, but, hey, it's my little prototyping section inside my own private Vim configuration file, so YOU CAN'T JUDGE ME. The second line defines the, uh, functionality of the, uh, function: it prints 'Hi, Fat.' (without the quotes) to the screen, and that's it. What's nice about this is that it's fast and low-impact. Third line, and this will shock you, says "this is the end of the function." The nnoremap we've seen before, it just means "in normal mode**, make the leader key, followed by tf, do two things: first, reload this file (that's the :source % bit), which will update the function we're playing with, and then, second, make the function named DoSomething() happen". The mnemonic is "test function".

**We all of course remember that "normal mode" means "not typing into the file, but navigating around the file". I.e., we can type stuff, and it won't (necessarily) add / subtract any text from the file, it'll just move around and whatnot.

So, obvs, the trick is to start adding lines. Bonus points for adding lines that do something you might want your computer to do.

So...what do we want it to do? The goal: automatic timestamps in a log of links I read and found interesting, which log looks halfway decent, preferably in Markdown formatting. (That's a plain text format that's pretty friendly on the eye and lends itself well to conversion to other formats, like HTML or whatever. We've seen this before.)

Vim, oddly, has a built-in function called strftime(), which asks the computer what time it is, and spits it out in the format of a "string" (whatever the he*l that is). If I understand it correctly, it is pretty much just a standard Unix function. Anyway, it has one million flags and a syntax I don't really understand, but the version I use is: strftime('%c'), which, if, say, called by :put =strftime('%c'), dumps something along the following lines into your file: "Sat 19 Jan 2019 12:52:29 AM PST" (without quotes). Now, that has a lot of extraneous bullshit in it, but the flags are difficult to understand (and platform-dependent), so fuck it, I'll stick with '%c'.

Now that we know Vim can tell us what the current time / date are, and even put it into a file, we can ... ask it to do so.

function! IncrementLog()
    let a:timestamp = strftime('%c')
    call(append(line('$'), [' ', join(['- ', 'a:timestamp']), join(['    * ', @+])]))
endfunction

(Hello append() my old friend; I've come to exploit you again...)

So, yeah. First line, say we have a function, with a name. Second line, define a variable, named timestamp and make the content of the variable the "get the time / date" bit we saw before. (The a: bit we can ignore for now: it's a (Ed. Note: the) way to limit the scope of the variable, which seems like a good idea with a variable name as generic and likely to occur widesly as timestamp.) Then: append(line('$') starts us off by saying "add this stuff AFTER the LAST line". This is good for a log, because it enforces a nice chronological order, like you want, in a log.

The next bit is a little bit of a lot. Stuff in [' ', ' '] brackets with commas separating stuff enclosed in quotes is, to Vim, a List. The join() function takes a List as an argument, and spits out a "string". You'll note that there's two of these join() functions, and that each is preceded by some stuff, and each is enclosed in those neat [' ', ' '] brackets with commas separating stuff enclosed in quotes. What append() is calling are two Lists. What this does is put each List on its own line. That's handy af.

The first List that append() takes as an argument is a blank line, then, as the second item, a formatting character, -, followed by a space, followed by our timestamp variable. The second List has as its first element a shit-ton of spaces, for indenting, followed by a formatting character, *, followed by @+, which is, of course, Vim-speak for "whatever is in the computer's shared clipboard". All together, that should result in, schematically:
[blank_line]
- [timestamp]
    * [contents_of_clipboard]

Or, more specifically:
- Sat 19 Jan 2019 01:21:38 AM PST
       *  https://prospect.org/article/return-strike

So that works, but it works in the file we're working in. That seems wrong. Seems like a log file should just be one file that lives somewhere stable. Also, this should be as automatic as possible. What I need is an easy way to call the IncrementLog() function, and a way to specify where to do so. That specified way should be easy to get to from wherever.

I fiddled for a long time with options that would automatically add to the log whenever I opened the log file, but then I realized that a file that adds to itself every time I open it ... is kind of a catastrophically dumb idea. I then fiddled for a short time with an option that would allow me to, from the command line, call a program and feed it an argument, which would be the clipboard. That seemed hard***, so instead I just fell back to the fact that I basically always have Vim open somewhere, and if I don't, it's as close as opening Terminal window and typing vim in there.

***And also more like a Python tutorial than a Vim way of doing things.

That meant I needed a way to find / open / write the log file, and, somewhere in there, at a semi-logical place, make the previous function happen. After quite a huge amount of fiddling, I discovered the following old VimScript adage: Just Put It in a Function, Sparklehorse, Then Call It (the Function).

function! s:UpdateReadingLog()
    :split ~/path/to/file.txt
    :call s:DoSomething()
    :execute ":wq"
endfunction

command! UpdateReadingLog :call s:UpdateReadingLog()

:nnoremap log :UpdateReadingLog<cr>

Fun! So: anywhere in Vim, mash the ol' leader key, followed by log, and the UpdateReadingLog() function opens a new window inside whatever window I'm working in, opens the file I want, runs the function we made earlier (which name I have changed to LogIt() for no good reason), then writes (saves) the file and quits it.

I'm on the fence about whether this really needs two separate functions. I may redo this at some point. But for now, it does almost exactly what I want, and seems to work on my work Mac and my home Zareason Linux laptop****, which is pretty neat. And what have we learned along the way?

  • Quick and easy prototyping of functions!
  • A relatively easy way to spit out the contents of a Vim register into a file—this is relevant because, earlier, we found a way to concatenate a whole bunch of files (or parts of those files) into one register
  • IDK, fun with variables??
  • Another possible use of this: imagine you want to research a new-to-you topic online and have a way to set up some breadcrumbs for yourself; you could bang in a new location for your log file and change your leader situation to like leadernote and Bob's your uncle
  • Anyway, I had a desire to learn something, and a desire to be able to log stuff I was reading, and I (a) learned how to (b) log stuff I was reading
  • So that's cool

****Interestingly, the auto-reload :source % command doesn't seem to work in MacVim. Deeply annoying, and cost me some time. Some possibility, of course, of user error.

Previous entries in Vim (for non-programmers):

1 comment:

Fat Contradiction said...

Here is a snippet that takes timestamps and makes them relative, which is kind of cool. https://www.codesections.com/blog/vim-timestamped/