tsunami

log in
history

IdleTime code

Luke Breuer
0001-01-01 05:00 UTC
tags: idletime

using System;
using System.IO;
using System.Timers;
using System.Collections.Generic;
using System.Text.RegularExpressions;


namespace IdleTime
{
    static class Program
    {
        static void Usage(bool showAdvanced)
        {
            //  79 dashes:
            //  ------------------------------------------------------------------------------
            Console.WriteLine((@"
                Author:  Luke Breuer
                Date:    01.07.08
                Purpose: Periodically poll the system for idle time information, as well as
                         information pertaining to the currently active window.
                Usage:
                         IdleTime.exe 
                             pollingInterval     in seconds
                             [logFile]           tee messages to this file
                             [-q --quiet]        no console output, aside from errors
                             [-c --class]        window class name
                             [-e --exe-name]     window exe name (no path)
                             [-t --title]        window title
                             [-o --other-title]  other window title (run with -? for details)

                         Note: the order in which the window information methods are
                               specified, either the -- version or in the switches value, 
                               controls the order the different fields are printed
                Examples:
                         IdleTime.exe 60 c:\idle.log
                         IdleTime.exe 1 -ecto
                " + (showAdvanced ? "" : @"
                Note:    Run IdleTime with the -? switch to get advanced help.
                ") + (!showAdvanced ? "" : @"
                Parsing: All fields should be delimited by two or more spaces.  All instances
                         of one or more whitespace characters in window fields get replaced 
                         with a single space.  Exe names and classes should not contain 
                         anything other than a single space at a time of whitespace, but this 
                         is not guaranteed or checked for.

                Other-title: Multiple document interface (MDI) programs sometimes do not 
                         reflect the currently active document in the main window title.
                         To counteract this, some window class paths have been hard-coded 
                         into this executable.  For example, if --other-title has been
                         specified and the foreground window is wndclass_desked_gsk, then 
                         the descendents will be searched according to the path set forth
                         by MDIClient|EzMdiContainer|DockingView: first a child window
                         of class MDIClient is searched for, ..., lastly, the title of the
                         first window with class DockingView is returned.  If a window is
                         not found, an error message will print out (starting with '>> '), 
                         specifying the class which was not found.")).Replace("\t\t\t\t", ""));
        }

        delegate void Action<TArg1, TArg2>(TArg1 arg1, TArg2 arg2);
        delegate void Action();

        static T FirstNonDefault<T>(T theDefault, params T[] list)
        {
            foreach (T v in list)
                if (!object.Equals(v, theDefault))
                    return v;

            return theDefault;
        }

        static TRet DefaultValueIfExceptionThrown<TRet, TEx>(Func<TRet> deferredExecute, Func<TEx, TRet> getDefaultValue)
            where TEx : Exception
        {
            try
            {
                return deferredExecute();
            }
            catch (Exception ex)
            {
                if (ex is TEx)
                    return getDefaultValue((TEx)ex);

                throw;
            }
        }

        const string TimeFormatString = "yy.MM.dd HH:mm:ss";
            
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        static void Main(string[] args)
        {
            throw new Exception();

            Func<string, int> argIndex = GetArgIndex(args);
            Predicate<string> argExists = arg => argIndex(arg) >= 0;
            bool helpAskedFor = Array.Exists(new[] { "-?", "/?", "--help" }, argExists);

            if (args.Length == 0 || helpAskedFor)
            {
                Usage(helpAskedFor);
                return;
            }

            int pollingInterval;

            if (!int.TryParse(args[0], out pollingInterval) || pollingInterval <= 0)
            {
                Console.Error.WriteLine("secondsBetweenOutput must be a positive integer");
                Usage(false);
                return;
            }

            string logFile = args.Length >= 2 && !args[1].StartsWith("-") ? args[1] : null;

            if (logFile != null && !IsValidFileName(logFile))
                return;

            bool quiet = argExists("--quiet");
            Func<IntPtr, string> print = GetPrinter(argIndex);
            Func<string> getIdleString = () => GetIdleString(print);
            Action<string, bool> output = Output(logFile, quiet);

            if (!quiet && (logFile == null || logFile != null && !File.Exists(logFile)))
                output("Created by IdleTime.exe; see Luke Breuer for details.\n\n" +
                    TimeFormatString + "  {minutes idle}  [{window information}]\n\n", false);

            output(getIdleString(), true);

            SetUpTimerAndWaitIndefinitely(pollingInterval, getIdleString, output);
        }

        private static Func<string, int> GetArgIndex(string[] args)
        {
            string flags = Array.Find(args, s => Regex.IsMatch(s, "^-[a-z]+$")) ?? "";

            Func<string, int> argIndex = arg =>
                FirstNonDefault(
                    -1,
                    Array.FindIndex(args, s => s.Equals(arg, StringComparison.OrdinalIgnoreCase)),
                    arg.StartsWith("--") && arg.Length >= 3
                        ? flags.IndexOf(arg[2])
                        : -1);
            return argIndex;
        }

        private static Func<IntPtr, string> GetPrinter(Func<string, int> argIndex)
        {
            Dictionary<string, string[]> classPathMap = GetClassPathMap();
            Func<string, string> escape = s => Regex.Replace(s ?? "", @"\s+", " ");
            Func<IntPtr, string> getOtherTitle = hWnd =>
            {
                string className = Win32.GetClassName(hWnd);
                string[] classPath;

                return !classPathMap.TryGetValue(className, out classPath)
                    ? null
                    : DefaultValueIfExceptionThrown<string, WindowNotFoundException>(
                        () => escape(Win32.GetTitle(Win32.FindWindowByClassPath(hWnd, classPath))),
                        ex => ">> " + ex.Message);
            };

            var prepareToPrint =
                Array.FindAll(Array.ConvertAll(new[] { 
                    new { Arg = "--class",       GetValue = (Func<IntPtr, string>)Win32.GetClassName },
                    new { Arg = "--title",       GetValue = (Func<IntPtr, string>)Win32.GetTitle },
                    new { Arg = "--exe-name",    GetValue = (Func<IntPtr, string>)Win32.GetProcessFileNameFromWindow },
                    new { Arg = "--other-title", GetValue = getOtherTitle },
                    }, p => new { Index = argIndex(p.Arg), p.GetValue }), p => p.Index >= 0);

            Array.Sort(prepareToPrint, (a, b) => a.Index.CompareTo(b.Index));

            return hWnd => string.Join("  ", Array.ConvertAll(prepareToPrint, p => escape(p.GetValue(hWnd))));
        }

        private static string GetIdleString(Func<IntPtr, string> print)
        {
            try
            {
                IntPtr hWnd = Win32.GetForegroundWindow();

                return hWnd == IntPtr.Zero
                    ? "GetForegroundWindow returned 0"
                    : print(hWnd);
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex.ToString());
                return string.Format("exception of type {0} thrown; see stderr", ex.GetType().Name);
            }
        }

        private static Action<string, bool> Output(string logFile, bool quiet)
        {
            Action<string, bool> output = (message, printTime) =>
            {
                message = message.Replace("\n", Environment.NewLine);

                if (printTime)
                    message = string.Format("{0:" + TimeFormatString + "} {1,6:0.0}{2}",
                        DateTime.Now,
                        Win32.GetLastInputTime() / 60.0,
                        message != null ? "  " + message : "");

                if (logFile != null)
                    File.AppendAllText(logFile, message + Environment.NewLine);

                if (!quiet)
                    Console.WriteLine(message);
            };
            return output;
        }

        private static void SetUpTimerAndWaitIndefinitely(int pollingInterval, Func<string> getIdle, Action<string, bool> output)
        {
            Timer t = new Timer(1000 * pollingInterval);

            t.Elapsed += (object sender, ElapsedEventArgs e) => output(getIdle(), true);

            t.Start();

            while (true) System.Threading.Thread.Sleep(100);
        }

        private static Dictionary<string, string[]> GetClassPathMap()
        {
            var classPathMap = new Dictionary<string, string[]>();

            classPathMap.Add("wndclass_desked_gsk", new[] { "MDIClient", "EzMdiContainer", "DockingView" });
            return classPathMap;
        }

        static bool IsValidFileName(string name)
        {
            try
            {
                FileInfo fi = new FileInfo(name);
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex.Message);
                return false;
            }

            return true;
        }
    }
}