最近は日が昇るのが早いからしばらくして薄暗いともう夕方かと思うけどまだ朝なんだよね。
我ながら意味不明で良い感じ。
もう夏至ですね。
最近は日が昇るのが早いからしばらくして薄暗いともう夕方かと思うけどまだ朝なんだよね。
我ながら意味不明で良い感じ。
もう夏至ですね。
ここ数日過去の紙資料をスキャンして処分しています。部屋の空きスペースを増やしたかったので。
手帳、仕事の印刷資料、手書きのメモなんかが主ですが、これが意外と量があって百科事典何冊分かにはなりそうです。紙を閉じているファイルなんかは無駄にかさばりますしね。
手帳は大学生くらいから2010年くらいまでありました。2009年くらいからあまり書かなくなっていて、2010年はほとんど書いてません。TODO管理用テキストの最初の項目が2010年から始まっているので、あまり必要なくなったのかもしれません。Android端末(HT-03AやSO-01B Xperia)を使い始めたのもこのあたりなので、それも関係しているかもしれません。まぁ、色々と行き詰まっていたというのもあるのかもしれません。
いつの頃からかA4コピー用紙でメモを書く習性がつきました。職場の机の右の引き出しには常に白紙のコピー用紙が大量に入っていて、何か考えるときはサッと取り出して考えをまとめながら書き、上にタイトルと日付を書いてパンチで穴を開けてファイルに閉じていました。そういうのがプロジェクト毎に沢山あります。最近は出来るだけデジタルだけで済ますようにしていますし、紙に書いたときでも最後はScanSnapでスキャンして紙の方は速やかに処分するようにしています。
そんなことで昔書いたものを見るわけですが、色々感じるところがありますね。なんだか切なくなったり、色々考えてるんだぁと感心したり、脳の中の忘れていた部分が刺激されてなんだか可能性が広がったような錯覚があったり。
まぁ、部屋は広くなるので新しいものが置けるようにはなるでしょう。
こういう日記もそうなのですが、基本的に自分が興味があることを書いているので後で読み返しても(自分にとっては)面白いんですよね。
カレンダーの過ぎた日に斜線を引くのが好きな私としては、過ぎた日が一目で分かるようにして欲しいのです。
例えば次のようなユーザースクリプトを作ってGreasemonkeyなりTampermonkey使えば出来るわけですが、まぁ、やりたいことの些細さに比べて面倒ですね。
(追記2018-11-14: デザインが変わって動かなくなっていたので Google Calendarで過ぎた日の色を変える(2018) を書きました)
(function(){ var PAST_COLOR = "#e0e0e0"; var PAST_BORDER_LEFT = "1px solid #d0d0d0"; var TODAY_COLOR = "#ffa"; var timer = null; function schedule(){ if(timer){ clearTimeout(timer); } timer = setTimeout(update, 100); } function update(){ if(document.getElementsByClassName("st-bg-today").length === 0){ return; } var bgs = Array.prototype.slice.call(document.getElementsByClassName("st-bg")); bgs.find(function(e,i,a){ if(e.className.match("st-bg-today")){ e.style.backgroundColor = TODAY_COLOR; return true; } else { //e.className += " st-bg-past"; e.style.backgroundColor = PAST_COLOR; e.style.borderLeft = PAST_BORDER_LEFT; return false; } }); var dtitles = Array.prototype.slice.call(document.getElementsByClassName("st-dtitle")); dtitles.find(function(e,i,a){ if(e.className.match("st-dtitle-today")){ e.style.backgroundColor = TODAY_COLOR; return true; } else { //e.className += " st-dtitle-past"; e.style.backgroundColor = PAST_COLOR; e.style.borderLeft = PAST_BORDER_LEFT; return false; } }); } var observer = new MutationObserver(schedule); observer.observe(document.body, {childList:true, subtree:true}); update(); })();
月表示や4週間表示の時、各日付の枠(td)には st-bg というクラスが付いています。 今日の日付をグレーにするためか、 st-bg-today というクラスも用意されていて、 今日の日付の枠は st-bg st-bg-today というクラスが付きます。
なので、クラスst-bgが付いている要素をgetElementsByClassNameで列挙して、 先頭からst-bg-todayが付いている要素に到達するまで背景色を変えることにしました。
今日の日付(st-bg-today)がカレンダー内にない場合もあります。 本来なら、過去のカレンダーか未来のカレンダーかを判別して、 全部色を変えるか全く色を変えないか判断しなければなりません。 面倒なので、st-bg-todayが無ければ何もしないようにしました。
日付の数字部分(td.st-dtitle)の背景には不透明な白が指定されています。 そのままだとせっかく背景の色を変えても数字部分だけ白くなってしまいます。 transparentを指定したら良いのかと思ったのですが、 カレンダーの1行がわずかに次の行と重なっているため、 数字部分の上の方が前の行の背景色になってしまいます。 div.month-rowのheightが4行の時は26%、5行の時は21%となっていて、 これを25%や20%にするとぴったり収まります。 なぜ1%余計に大きくしているのでしょうか。 無理矢理1%小さくする方法も考えたのですが、 Googleは今日の日付の色を変えるために.st-bg-todayと.st-dtitle-todayの両方の backgroundを指定していますので、それにならって.st-dtitleの色も.st-bg と同様に変更することで解決しました。
最初はst-bg-pastとst-dtitle-pastというクラスを付加するようにしたのですが、 別途Stylish等でユーザースタイルを指定するのが面倒だったので 直接ユーザースクリプトで色を変えるようにしてしまいました。
MutationObserverのコールバック頻度はどのように決まっているのでしょうか。 今ひとつ分からなかったので、タイマーを併用して最後の更新から100ms後に 色替えを実行することにしました。
Googleのプロダクトはかゆいところに全く手が届かないのが困りものですよね。 あまり細かいカスタマイズが必要ならGoogle Calendar APIを使用して 自分でカレンダーを作るなり、他の既成のカレンダーを探すなりした方が良いかも しれません。
Google Calendarとやりとり出来るようになったので、org-modeとGoogle Calendarとの間で必要となる道具立てを作成しています。
色々作成中ですがとりあえず。
misohena/gcal: Google Calendar Utilities for Emacs
gcal-org.el というのが中心になるのですが、その中のgcal-oeventオブジェクトがGoogle Calendarへ登録される予定を表します。
gcal-oevent(以下単にoevent)は、org文書を解析して得られた1つの予定(event)を表現するオブジェクトです。
オブジェクトは、例えば以下のようなコードで作成できます。
(setq oe (make-gcal-oevent :id "a84717b2-7c0b-4549-80f4-9477d14f975f" :ord 0 :summary "田中さんと打ち合わせ" :ts-prefix "DEADLINE" :ts-start '(2016 2 3 nil nil) :ts-end '(2016 2 4 nil nil) :location "東京駅"))
また、org文書を解析して(そのリストが)得られたり、
(gcal-org-parse-file "~/my-schedule.org") ;;指定ファイルから (gcal-org-parse-buffer) ;;現在のバッファから
オブジェクトをファイルへ保存して、それを読み込んで得られたり、
;; my-schedule.orgを解析したものをmy-schedule.gcal-cacheへ保存 (gcal-oevents-save "~/my-schedule.gcal-cache" (gcal-org-parse-file "~/my-schedule.org")) ;; my-schedule.gcal-cacheからoeventのリストを読み込み (gcal-oevents-load "~/my-schedule.gcal-cache")
Google Calendar上のカレンダーから読み込んで得られたりします。
;; カレンダー(ID:example@gmail.com)から予定を読み込んでoeventのリストとして返す (gcal-org-pull-oevents "example@gmail.com")
プロパティの取得は (gcal-oevent-プロパティ名) で行えます。
(gcal-oevent-id oe) ;;=> "a84717b2-7c0b-4549-80f4-9477d14f975f" (gcal-oevent-summary oe) ;;=> "田中さんと打ち合わせ"
このオブジェクトはorg文書中の一つの(アクティブ)タイムスタンプに対応します。一つのヘッドラインに対応するのでは ありません 。 org-agendaが行うように、SCHEDULED、DEADLINE、その他記事中のアクティブタイムスタンプ一つにつき一つの予定をGoogle Calendarへ登録したかったからです。
そのため :id の他に :ord というプロパティを持っています。このプロパティは一つのエントリー中に現れたアクティブタイムスタンプの順番を表します。
Google Calendarの予定のIDはbase32hexで使う範囲の文字(0-9a-v)しか受け付けません(ハイフンが入っているとダメです)。 また、デフォルトではUUIDをbase32hexでエンコードしたものをIDとしています。
従って、oeventをGoogle Calendarへ登録するときは:idをbase32hexでエンコードしたものへ変換することにしました。
(downcase (gcal-uuid-to-base32hex "a84717b2-7c0b-4549-80f4-9477d14f975f")) ;; => "l13hfcjs1d2kj07kihrt2jsnbs" (gcal-uuid-from-base32hex "l13hfcjs1d2kj07kihrt2jsnbs") ;; => "a84717b2-7c0b-4549-80f4-9477d14f975f"
しかしこれだけだと1エントリー中に複数のタイムスタンプがある場合にIDが重複してしまいます。
それを避けるために :ord が1以上の時は、その数をIDの後ろに付加することにしました。幸い文字数の制限は緩いので。
(gcal-oevent-gevent-id
(make-gcal-oevent
:id "a84717b2-7c0b-4549-80f4-9477d14f975f"
:ord 1
... )) ;;=> "l13hfcjs1d2kj07kihrt2jsnbs00001"
(gcal-oevent-id-ord-from-gevent-id "l13hfcjs1d2kj07kihrt2jsnbs00001")
;; => ("a84717b2-7c0b-4549-80f4-9477d14f975f" . 1)
:ord が0のときは付加しません。Google Calendar上で作成した予定を取り込むときはその方が都合が良いからです。取り込んだイベントをorg文書中にインポートした後、元のGoogle Calendar上のイベントのIDを番号付きに修正する必要が出てしまいますので。
:ts-start や :ts-end は (年 月 日 時 分) というリストで表現します。このリストは gcal-ts- で始まる関数で色々な処理が出来るようになっています。
時 分 はnilの場合があります。時間の入っていないタイムスタンプに対応します。
(gcal-ts-date-only '(2016 5 29 nil nil)) ;;=> t <2016-05-29 Sun> (gcal-ts-date-only '(2016 5 29 12 34)) ;;=> nil <2016-05-29 Sun 12:34>
ややこしいのはts-endの扱いで、Google Calendar側では終了時刻はその時刻自身を含まない(exclusive)とされていますが、org-mode側では曖昧であることです。 org-modeは日付のみの場合はinclusive、時刻を含む場合はexclusiveなのだと思うので、それを前提に変換しています。
(gcal-ts-end-exclusive '(2016 5 1 nil nil) '(2016 5 1 nil nil)) ;; => '(2016 5 2 nil nil) 1day <2016-05-01 Sun> (gcal-ts-end-exclusive '(2016 5 1 nil nil) '(2016 5 2 nil nil)) ;; => '(2016 5 3 nil nil) 2days <2016-05-01 Sun>--<2016-05-02 Mon> (gcal-ts-end-exclusive '(2016 5 1 10 00) '(2016 5 1 10 00)) ;; => '(2016 5 1 10 0) 0hour <2016-05-01 Sun 10:00> (gcal-ts-end-exclusive '(2016 5 1 10 00) '(2016 5 1 12 00)) ;; => '(2016 5 1 12 0) 2hours <2016-05-01 Sun 10:00-12:00> (gcal-ts-end-exclusive '(2016 5 1 10 00) '(2016 5 2 10 00)) ;; => '(2016 5 2 10 0) 24hours <2016-05-01 Sun 10:00>--<2016-05-02 Mon 10:00> ;;逆は gcal-ts-end-inclusive
oeventをGoogle Calendar APIへ送る(gcal-events-insertへ渡す)ための形式へ変換するには gcal-oevent-to-gevent を、その逆は gcal-oevent-from-gevent を使用します。
;; Send a oevent (gcal-events-insert "example@gmail.com" (gcal-oevent-to-gevent oevent)) ;; Receive a event as oevent (gcal-oevent-from-gevent (gcal-events-get "example@gmail.com" "l13hfcjs1d2kj07kihrt2jsnbs"))
oeventのリストは gcal-org-push-oevents と gcal-org-pull-oevents で送受信できます。
;; Send all active timestamps in my-schedule.org (gcal-org-push-oevents "example@gmail.com" (gcal-org-parse-file "~/my-schedule.org") nil) ;; Receive (gcal-org-pull-oevents "example@gmail.com")
gcal-org-push-oeventsは差分抽出機能を備えており、旧リストから新リストへの差分(追加、削除、更新)のみをGoogle Calendarへ送ることが出来ます。
;; my-schedule.orgだけにあるものを追加し、.oldだけにあるものを削除し、内容が変わったものをパッチする。 (setq result-events (gcal-org-push-oevents "example@gmail.com" (gcal-org-parse-file "~/my-schedule.org") ;;new-events (gcal-org-parse-file "~/my-schedule.org.old");;old-events )) ;; result-eventsはサーバ上に残った予定のリスト。 ;; エラーが無ければnew-eventsと同じ。 ;; 追加に失敗すればその予定を載らないし、更新に失敗すればその予定は旧状態、削除に失敗すればその予定は残る。 ;; my-schedule.orgにある予定を全て削除する。 ;; new-eventsがnil(空リスト)なので、old-events内の予定に対応するものは一つもないので。 (gcal-org-push-oevents "example@gmail.com" nil (gcal-org-parse-file "~/my-schedule.org"))
org文書からoeventのリストを得る(解析する)には次のようにします。
(gcal-org-parse-file "~/my-schedule.org")
(gcal-org-parse-buffer)
逆にoeventを文字列化してorg文書へ挿入するには次のようにします。 (※ただし、:ordのことを考慮していないので注意が必要です)
;; 挿入先のorg文書を開いた状態で実行すること (gcal-org-insert-string-after-headline (gcal-oevent-format oevent) "Inbox")
gcal-oevent-format は何も引数を指定しなければ gcal-org-oevent-template 変数に書いてあるテンプレートを使ってoeventを文字列化します。
gcal-org-oevent-template "** %{summary}\nSCHEDULED: %{timestamp}\n:PROPERTIES:\n :ID: %{id}\n :LOCATION: %{location}\n:END:\n" "org-mode text representation of oevent."
oeventオブジェクトを介さずに直接やりとりする関数も作っています。
;; キャッシュ~/my-schedule.gcal-cacheから~/my-schedule.orgへの差分を ;; カレンダーexample@gmail.comへ適用します。 ;; 成功した更新はキャッシュへ反映されます。 (gcal-org-push-file "example@gmail.com" "~/my-schedule.org" "~/my-schedule.gcal-cache") ;; キャッシュmy-schedule.gcal-cacheからカレンダーexample@gmail.comへの差分を ;; ~/my-schedule.orgへ適用します。 ;; 新しく追加された予定はヘッドライン"Inbox"の下に挿入します。 ;; 成功した更新はキャッシュへ反映されます。 (gcal-org-pull-to-file "example@gmail.com" "~/my-schedule.org" "Inbox" "~/my-schedule.gcal-cache")
pullは作り途中で、更新に対応していません(2016-05-29現在)。org文書中の一部の場所だけ書き換えないといけないので。
gcal-oevents-diffを使用すると二つのoeventリストを比較できます。
それぞれの予定を :id と :ord をキーにマッチングを行い、変化(追加、削除、変更、そのまま)を見つけ出し、それに対応する関数を呼び出します。
(gcal-oevents-diff (gcal-oevents-load "~/my-schedule.org") ;;old-events (gcal-org-pull-oevents "example@gmail.com") ;;new-events (lambda (old-oe new-oe) (insert (format "mod %s => %s\n" old-oe new-oe))) (lambda (new-oe) (insert (format "add %s\n" (gcal-oevent-summary new-oe)))) (lambda (old-oe) (insert (format "del %s\n" (gcal-oevent-summary old-oe)))) (lambda (oe) (insert (format "not change %s\n" (gcal-oevent-summary oe)))))