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;
}
}
}