2011/07/29


普段twitterするときはtwitvimというvimscriptを使ってvimからやってるのだけど、たいがいこういうのって自分の思う機能が実装されていなかったり、違う実装になっていたりする。
TwitVim - Twitter client for Vim : vim online

TwitVim is a Vim plugin that allows you to post to Twitter and view Twitter timelines.

http://www.vim.org/scripts/script.php?script_id=2204
twitvim - Twitter Client for Vim - Google Project Hosting

A Twitter client written in Vim script. See http://vim.sourceforge.net/scripts/script.php?script_id=...

http://code.google.com/p/twitvim/
twitvimも例外なく僕の欲しい機能がなかったりする。例えば
  • <leader>vなんかでそのツィートのpermalinkを開いてほしい
  • 入力中に今何文字入力しているか確認したい
  • 公式RTじゃなくて軟式RT(非公式RT)が使いたい
てな感じ。でもコードを弄ると最新バージョン出た時にマージが大変。
こういうときは外側からそのスクリプトを弄る。

でもtwitvimの関数の殆どは"s:"で始まるスクリプトスコープ。外側からは触れません。ただしhack無しの場合。
スクリプトスコープの関数は、実はSNRという装飾が付いた関数名になっていて、例えば function s:launch_browser(url)
と宣言された関数を外部から呼び出すには <SNR>39_launch_browser
と装飾キーとスクリプト番号を付与してあげれば呼び出しが可能。このスクリプト番号とはスクリプトファイル1つに対して1つ割り当てられた数値。
:scriptnames
で確認出来ます。またファイル名でひっかけなくてもcommandコマンドの結果から抜き取る事も出来ます。 silentredir => commands
silentcommand PosttoTwitter
silentredir END
let g:twitvim_sid = substitute(split(commands, "\n")[-1], '^.*<SNR>\(\d\+\)_.*$''\1''')
あとはこのSIDを使って、mapしたり関数を呼び出したりすればok。
例えば↑のpermalinkを開く改造であれば function! TwitVimShowInBrowser()
  silentredir => json
  silent! TwitVimShowCurbuffer
  silentredir END
  let curbuffer = eval(substitute(json, "\n"'''g'))
  let id = get(curbuffer.statuses, line('.'))
  if id != 0
    let user = substitute(curbuffer.buffer[line('.')-1], ':.*''''')
    let url = "http://twitter.com/".user."/statuses/".id
    call function('<SNR>'.g:twitvim_sid.'_launch_browser')(url)
  endif
endfunction
この様に書ける訳です。スクリプトスコープで無いと参照する事が出来ない関数を呼び出しています。 こうかけば非公開関数であるはずのlaunch_browser()を呼び出す事が出来ます。
同じ要領で、文字数をカウントしてみましょう。twitvimはコマンドラインからツィートを投げたりするのですが、コマンドラインは入力のフックがとても難しいのです。 function! TwitVimShowMessageLength()
  let line = getcmdline()
  if expand('<sfile>') !~ 'CmdLine_Twitter'
    return line
  endif
  echo ""
  redraw
  echohl WarningMsg
  echo function('<SNR>'.g:twitvim_sid.'_mbstrlen')(line) . " character(s)"
  echohl None
  redraw
  silent sleep 1
  return line
endfunction
cnoremap <c-g> <c-\>eTwitVimShowMessageLength()<cr>
getcmdline()関数で現在のカーソル位置情報を退避しておき、現在の文字数を非公開関数mbstrlen()から得て表示。1秒経ったら元に戻すという物。これで :PosttoTwitter
とした後、タイプ中に「今何文字目だろう?確認するには投げてみるしかないし、長すぎたら消えて履歴から戻せないんだよな。」って時でもCTRL-gをタイプすれば文字数が分かる様になります。

さて、最後の軟式RTについてですが実はtwitvimには手動RTのインタフェースが既に定義されている。でもキーマッピングされていない為にどこからも呼び出す事が出来ない。おそらく公式RTポスト処理を実装した時に軟式RTポスト処理をそのままにしたのだろう。 exe "nnoremap <buffer> <leader>q :call <SNR>".g:twitvim_sid."_Retweet()<cr>"
こうすれば呼び出せる。
全体のコードだと以下の様になる。
functions:TwitVimSetup()
  if has('win32')
    let g:twitvim_browser_cmd = 'rundll32 url.dll,FileProtocolHandler'
  else
    let g:twitvim_browser_cmd = 'w3m'
  endif

  silentredir => commands
  silentcommand PosttoTwitter
  silentredir END
  let g:twitvim_sid = substitute(split(commands, "\n")[-1], '^.*<SNR>\(\d\+\)_.*$''\1''')

  function! TwitVimShowIcon()
    silentredir => json
    silent! TwitVimShowCurbuffer
    silentredir END
    let curbuffer = eval(substitute(json, "\n"'''g'))
    let id = get(curbuffer.statuses, line('.'))
    if id != 0
      let user = substitute(curbuffer.buffer[line('.')-1], ':.*''''')
      exec "OpenBrowser http://api.dan.co.jp/twicon/".user."/bigger"
    endif
  endfunction

  function! TwitVimShowInBrowser()
    silentredir => json
    silent! TwitVimShowCurbuffer
    silentredir END
    let curbuffer = eval(substitute(json, "\n"'''g'))
    let id = get(curbuffer.statuses, line('.'))
    if id != 0
      let user = substitute(curbuffer.buffer[line('.')-1], ':.*''''')
      let url = "http://twitter.com/".user."/statuses/".id
      call function('<SNR>'.g:twitvim_sid.'_launch_browser')(url)
    endif
  endfunction

  function! TwitVimShowMessageLength()
    let line = getcmdline()
    if expand('<sfile>') !~ 'CmdLine_Twitter'
      return line
    endif
    echo ""
    redraw
    echohl WarningMsg
    echo function('<SNR>'.g:twitvim_sid.'_mbstrlen')(line) . " character(s)"
    echohl None
    redraw
    silent sleep 1
    return line
  endfunction
  cnoremap <c-g> <c-\>eTwitVimShowMessageLength()<cr>

  function! TwitVimUserMap()
    nnoremap <buffer> <leader>t :FriendsTwitter<cr>
    nnoremap <buffer> <leader>T :RepliesTwitter<cr>
    exe "nnoremap <buffer> <leader>q :call <SNR>".g:twitvim_sid."_Retweet()<cr>"
    nnoremap <buffer> <leader>i :call TwitVimShowIcon()<cr>
    nnoremap <buffer> <leader>V :call TwitVimShowInBrowser()<cr>
    if has('win32') && !has('gui_running')
      exe "noremap <char-206><char-199> ".maparg("<c-pageup>")
      exe "noremap <char-206><char-210> ".maparg("<c-pagedown>")
    endif
  endfunction

  autocmd FileType twitvim call TwitVimUserMap()

  silentdelcommand UseVimball
endfunction
autocmd VimEnter * call s:TwitVimSetup()
vimrcに足して使ってます。
ちなみにUseVimballコマンドを消しているのはUserTwitterと被って補完を間違う事が多い為。

この方法さえ分かれば、貴方もVim Hackerですね!どんどん他人のスクリプトを改造してしまいましょう!
Posted at by



2011/07/28


最近earthquakeという、rubyで書かれていて端末上で動作するtwitterクライアントをWindowsで動かそうと色々弄ってます。作者のjugyoさんにコミットビットも貰ってwork-on-windowsブランチで作業してます。earthquake側の修正はだいたいイケてるはずなんですが、問題はreadlineという行編集ライブラリで問題が発生。
ちゃんと書くと、C言語で書かれたreadlineをwrapしているreadlineモジュールじゃなくて、rubyinstallerに標準添付されたPure RubyScriptなモジュール。中身はUNIXのコードとWindowsのコードが入り乱れていて、rbreadline.rbなんか8686行もある大作。

まぁPure RubyScriptでCの真似事をしようってんだからこうなるよね...って感じ。
ただバイト長とキャラクタ数と、文字幅の扱いが間違ってて、Windows-31JなDBCSなんかではちゃんと動かない。パッチ書いて「問題があるんだよ。気付いて!」ってつもりでpull request送ったらいきなりIssue trackerでcloseされてカチンと来たので「その態度はいかがなもんかと思う」的なコメントをした。そしたら「まずテストを書け」との事だったので書いた。

しかしながら、上記の間違いを直すとしてreadlineの正しい動きをテストするって難しい。関数単体なら出来るけどUIのテストはどの言語でも苦しむ。おまけにreadlineはreadline()メソッドを呼び出してる最中は、テスターが止まってしまう。終えるにはユーザの入力が必要。
じゃぁユーザの入力を横取りしてやんよ!って事で、rl_get_char()をぶんどる事にした。
module RbReadline
  #...

  alias :old_rl_get_char :rl_get_char
  def rl_get_char(*a)
    c = old_rl_get_char(*a)
    c.force_encoding("ASCII-8BIT"if c
    @last_xy = xy
    return (c || EOF)
  end

  def rl_get_char=(val)
    for c in val.reverse
      _rl_unget_char(c)
    end
  end

  module_function :old_rl_get_char:rl_get_char:"rl_get_char="
end
こんな感じにRbReadline.rl_get_char()をMix-inで上書きしてやる。こうすれば、外からキー入力を差し替えられる。 RbReadline.rl_get_char = [""""""""""]
buf = Readline.readline("")
これさえ出来れば、文字入力させて最後のカーソル位置が正しい位置にいる事を確認出来る。カーソルの位置はGetConsoleScreenBufferInfo()で得られるのでWin32API使ってゴリゴリ取った。また、最終的な入力結果が文字化けしていない事を確認する為にReadConsoleOutputCharacter()も使った。
全体のコードは以下の様になった。
# encoding: CP932
require 'test/unit'
require 'rb-readline'
require 'Win32API'

module RbReadline
  @GetStdHandle = Win32API.new("kernel32","GetStdHandle",['L'],'L')
  @hConsoleHandle = @GetStdHandle.Call(STD_OUTPUT_HANDLE)
  @GetConsoleScreenBufferInfo = Win32API.new("kernel32","GetConsoleScreenBufferInfo",['L','P'],'L')
  @ReadConsoleOutputCharacter = Win32API.new("kernel32","ReadConsoleOutputCharacter",['L','P','L','L','P'],'I')

  alias :old_rl_get_char :rl_get_char
  def rl_get_char(*a)
    c = old_rl_get_char(*a)
    c.force_encoding("ASCII-8BIT"if c
    @last_xy = xy
    return (c || EOF)
  end

  def rl_get_char=(val)
    for c in val.reverse
      _rl_unget_char(c)
    end
  end

  def last_xy
    @last_xy
  end

  def last_xy=(val)
    @last_xy = val
  end

  def xy
    csbi = 0.chr * 24
    @GetConsoleScreenBufferInfo.Call(@hConsoleHandle,csbi)
    [csbi[4,2].unpack('s*').first, csbi[6,4].unpack('s*').first]
  end

  def get_line(l)
    line = 0.chr * 80
    length = 80
    coord = l << 16
    num_read = ' ' * 4
    @ReadConsoleOutputCharacter.Call(@hConsoleHandle,line,length,coord,num_read)
    line.force_encoding("Windows-31J")
  end

  module_function :old_rl_get_char:rl_get_char:"rl_get_char="
end

class TestReadline < Test::Unit::TestCase

  def setup
    Readline::HISTORY << "世界".force_encoding("ISO-8859-1")
    Readline::HISTORY << "abc".force_encoding("ISO-8859-1")
    Readline::HISTORY << "bcdef".force_encoding("ISO-8859-1")
    RbReadline.rl_get_char = []
    RbReadline.last_xy = RbReadline.xy
    puts
  end

  def test_cursor_position_normal
    a = RbReadline.xy
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 2, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_mix
    a = RbReadline.xy
    buf = Readline.readline("$$$")
    b = RbReadline.last_xy
    assert_equal 5, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_insert_single_width
    a = RbReadline.xy
    RbReadline.rl_get_char = ["a""b""c""d""e"]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 7, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_insert_double_width
    a = RbReadline.xy
    RbReadline.rl_get_char = [""""""""""]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 12, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_previous_history
    a = RbReadline.xy
    RbReadline.rl_get_char = ["a""\340H""\340H"]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 5, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_next_history
    a = RbReadline.xy
    RbReadline.rl_get_char = ["a""\340H""\340H""\340P"]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 7, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_history_include_multibyte
    a = RbReadline.xy
    RbReadline.rl_get_char = ["a""\340H""\340H""\340H"]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 6, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_insert_into
    a = RbReadline.xy
    RbReadline.rl_get_char = ["""""""\340K""\340K""a"]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 5, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
    assert_equal "$あaいう"RbReadline.get_line(b[1])
  end
end
僕がpull requestしたpatchもtest_cursor_position_insert_intoはNG出るのでこれを直していこうと思う。
Posted at by



2011/07/27


Emacsはインライン画像も表示出来て、ブラウザにもなって、vimでは到底真似出来ない芸当が出来るの事は、vimmerであれば皆知ってる事だとは思うんだけど...
昨日こんな動画があることを教えて貰った。
GVimでとある科学の超電磁砲OP「only my railgun」‐ニコニコ動画(9)

たくさんプログラミングしてった。ちょっと疲れた。ちなみに、これはリアルタイムのレンダリングではない。私自分のパソコンには2fpsぐらいです。64色でお楽しみ。感想はこちら(中国語だが):http://www.fancymouse.net/blog/2009/12/20/gvim_railgun_op/

http://www.nicovideo.jp/watch/sm9143016
「えっ?どうやってるの?」って思われた方もいるかもしれない。
実はコラでもなんでもない。Vimで出来る(正しくはGVim)。Vimは画像を表示する事は出来ないが、シンタックスハイライトにかけては他のエディタと比べ物にならないくらい優秀だ。XPM画像ファイル(X Pixmap)はASCIIだけで書かれたC言語のヘッダファイルなのだが、先頭に色定義、後続にその定義を使ったデータ部となっている為、VimではXPM画像ファイルを開くとさも画像ファイルであるかの様に見える。もちろんフォントの大きさが1ドットになるので若干縦長にはなる。
gvim-xpm
この仕組みを使えば、Vimで動画が再生出来る。もちろん↑の動画作者も言ってる通り、リアルタイムじゃないのであしからず。なおこの手順は↑の動画を見てmattnが勝手に推測した方法なので若干手法が違うかもしれない。

まず、動画を再生しそれを連番画像ファイルとして出力する。ここではmplayerを使うと楽。
# mplayer /home/mattn/動画/仮面ライダーディケイド変身.avi -vo jpeg
放っておくとどこまでもjpeg画像を出力しまくるので適度に止める。
次にこのjpegファイル群をxpm画像ファイルへ変換する。こんな場合はシェルスクリプトにお任せ。
mkdir out
/bin/ls *.jpg |\
while read -r line; do
    FNAME=`basename $line .jpg`
    convert $line -resize 150x100 out/$FNAME.xpm ;
done
convertはImageMagickに付属されている。
さて、outディレクトリにxpmファイル群が出力されたので、一枚適当に開いて最下行までを表示した時に全てのドットが収まる様に、フォントとウィンドウ幅を調整する。フォントが小さすぎてウィンドウ幅が分からないのであれば、「:new」して無名バッファを作り、挿入モードで「<c-r>=&columns . " " . &lines . " " . &guifont」と入力してどこかに保存してしまえばいい。
表示しきったらこれをGVimの枠付きでキャプチャを取る訳だが、数十枚ある画像を手作業ではやってられない。ここも自動化する。
以下のvimスクリプトを書いた。
set guifont=Symbol\ 1
set columns=300
set lines=90
unlet! images
let images = split(globpath(".", "*.xpm"), "\n")
for image in images
  exe "e ".image
  normal! G
  redraw
  silent! exec "!scrot -u"
  bw!
endfor
なるべく正方形に近いフォントを選んだつもり。scrotはスクリーンキャプチャで、何時も愛用している物。デスクトップ全体とかエコじゃない事はしたくないので「-u」オプションを付けて、フォーカスのあるウィンドウだけキャプチャを取った。
これを実行すれば、現在のフォルダからxpmファイル群を一つずつ開き、最下行に移動し、画面更新後にキャプチャを取得、バッファを終了、という動きになる。ちなみに今回の例だと、1画像を表示仕切るのに4秒程度かかったので、合計すると数十分掛かった。

さて、これでscrotが生成したキャプチャが出来上がるので、合成する。こんな場合はmencoderが役に立つ。
mencoder mf://*scrot.png -mf w=320:h=335:fps=5:type=png -ovc copy -o output.avi
一瞬にしてoutput.aviが生成された。
fpsは適度に調整が必要。

それでは出来上がった物を...

続きを読む...

Posted at by