(* main.ml - Don Yang (uguu.org) 10/27/07 *) (* Maximum number of recent events to show *) let max_recent_events = 5;; (* Regular expressions for parsing commands *) let cmd_abbrev_exec = Str.regexp "^\\([^ ]+\\) *= *\\([^ ].*\\)$";; let cmd_abbrev_noexec = Str.regexp "^ *# *\\([^ ]+\\) *= *\\([^ ].*\\)$";; let cmd_find_prefix = Str.regexp "^\\.\\(.*\\)$";; let cmd_find_substr = Str.regexp "^/\\(.*\\)$";; let cmd_match_time = Str.regexp ("^\\([0-9][^ ]+ [0-9]+:[0-9][^ ]+\\|[0-9][^ ]+\\) +" ^ "\\([^ ].*\\)$");; (* Program name *) let prog = "keine";; (* Check if stdin is a tty. This requires OCaml 3.10.0 or higher, otherwise it won't compile. If you don't want to upgrade, setting this to true will do the right thing most of the time. *) let stdin_is_tty = Unix.isatty (Unix.descr_of_in_channel stdin);; (* Show abbreviations *) let show_abbreviations dict = if (Hashtbl.length dict) > 0 then ( print_string "Abbreviations:\n"; let a = Hashtbl.fold (fun x y z -> (x ^ " = " ^ y) :: z) dict [] in List.iter (fun xy -> Printf.printf " %s\n" xy) (List.fast_sort compare a) ) else ();; (* Show recent events *) let rec show_recent_events reverse_log index = if index >= max_recent_events then () else match reverse_log with (time, event)::tail when event <> Log.start_marker && event <> Log.end_marker -> ( if index = 0 then print_string "Recent events:\n" else (); show_recent_events tail (index + 1); Printf.printf "%s %s\n" (Time.encode_time time) event ) | (_, _)::tail -> show_recent_events tail index | _ -> ();; (* Show help message *) let show_help reverse_log dict = print_string ( "Commands:\n" ^ " task Record context switch to \"task\"\n" ^ " . prefix Record context switch to previous task " ^ "matching \"prefix\"\n" ^ " / substr Record context switch to previous task " ^ "containing \"substr\"\n" ^ "\n" ^ " key=task Define abbreviation and record task\n" ^ " # key=task Define abbreviation without recording task\n" ^ "\n" ^ " ? Show commands and abbreviations\n" ^ " ! Reload events and abbreviations from file\n" ^ " (empty string) Show stats\n" ^ " (EOF) Exit\n" ); show_abbreviations dict; show_recent_events reverse_log 0;; (* Record event to log *) let record_event time event reverse_log log_file echo = reverse_log := (time, event) :: !reverse_log; let line = (Time.encode_time time) ^ " " ^ event ^ "\n" in Log.append_block_to_file line log_file; if echo then print_string line else ();; (* Record abbreviation to log and dictionary *) let record_abbreviation command dict log_file = let key = Str.matched_group 1 command in let value = Str.matched_group 2 command in Hashtbl.replace dict key value; Log.append_block_to_file (Printf.sprintf "# %s = %s\n" key value) log_file;; (* Execute command from log or user *) let execute_command time command echo reverse_log dict log_file = if Str.string_match cmd_abbrev_exec command 0 then ( (* Add and expand abbreviation *) record_abbreviation command dict log_file; record_event time (Str.matched_group 2 command) reverse_log log_file echo ) else if Str.string_match cmd_abbrev_noexec command 0 then ( (* Add abbreviation without recording event *) record_abbreviation command dict log_file ) else if Str.string_match cmd_find_prefix command 0 then ( (* Find previous event matching prefix *) let prefix = Str.matched_group 1 command in try let event = Log.find_last_event_prefix !reverse_log prefix in record_event time event reverse_log log_file echo with Not_found -> Printf.printf "No previous event with prefix %s\n" prefix ) else if Str.string_match cmd_find_substr command 0 then ( (* Find previous event containing substring *) let key = Str.matched_group 1 command in try let event = Log.find_last_event_substr !reverse_log key in record_event time event reverse_log log_file echo with Not_found -> Printf.printf "No previous event with substring %s\n" key ) else ( (* Expand command *) try let event = Hashtbl.find dict command in record_event time event reverse_log log_file echo with Not_found -> record_event time command reverse_log log_file echo );; (* Load log file from disk, or initialize new log file *) let open_log log_file = try ( let in_channel = open_in_bin log_file in let (reverse_log, last_time, dict) = Log.read_file_to_log in_channel in Printf.printf "Loaded %d events from %s\n" (List.length reverse_log) log_file; close_in in_channel; (reverse_log, last_time, dict) ) with Sys_error _ -> ( Printf.printf "Starting new log file %s\n" log_file; ([], 0.0, (Hashtbl.create 128)) );; (* Stop writing to log and exit *) let close_log reverse_log dict log_file = execute_command (Unix.time ()) Log.end_marker false reverse_log dict log_file; Log.append_block_to_file "\n" log_file; Printf.printf "\nStopped logging to %s\n" log_file; exit 0;; (* Execute special commands, return true if command was indeed special *) let execute_special_command command reverse_log last_time dict log_file = if command = "" then ( (* Temporary push a dummy event to log to mark the current event as still ongoing. *) Report.show_report (List.rev ((Unix.time(), Log.end_marker) :: !reverse_log)); true ) else if command = "?" then ( show_help !reverse_log !dict; true ) else if command = "!" then ( let (l, t, d) = open_log log_file in reverse_log := l; last_time := t; dict := d; true ) else ( false );; (* Execute regular commands *) let execute_normal_command command reverse_log last_time dict log_file = let current_time = Unix.time () in if Str.string_match cmd_match_time command 0 then ( let t = Str.matched_group 1 command in let command = Str.matched_group 2 command in let new_time = Time.decode_time !last_time t in if new_time < 0.0 || new_time > current_time then ( print_string "Invalid timestamp\n" ) else ( execute_command new_time command true reverse_log !dict log_file; last_time := new_time ) ) else ( execute_command current_time command true reverse_log !dict log_file; last_time := current_time );; (* Read commands over and over until EOF or Ctrl+C *) let input_loop reverse_log last_time dict log_file = print_string "Enter ? for help\n"; execute_command (Unix.time ()) Log.start_marker false reverse_log !dict log_file; Sys.catch_break true; try while true do ( if stdin_is_tty then print_string "> " else (); let command = Strutil.clean_string (read_line ()) in ( if execute_special_command command reverse_log last_time dict log_file then () else execute_normal_command command reverse_log last_time dict log_file ) ) done with End_of_file | Sys.Break -> (); (* Stop logging *) close_log reverse_log !dict log_file;; (* Main program entry, decide what to do based on number of command line arguments. *) let main () = if Array.length Sys.argv = 1 then ( (* Non-interactive use *) if stdin_is_tty then ( (* Print help message and exit *) print_string ( "Usage:\n" ^ " " ^ prog ^ " log.txt " ^ "Start interactive session, recording to log.txt\n" ^ " " ^ prog ^ " < log.txt " ^ "Process log.txt, print summary and exit.\n" ) ) else ( (* Process events from stdin and print report *) Report.show_report (List.rev (Log.read_stdin_to_log ())) ) ) else ( (* Interactive use *) let log_file = Sys.argv.(1) in let (reverse_log, last_timestamp, dict) = open_log log_file in input_loop (ref reverse_log) (ref last_timestamp) (ref dict) log_file );;