Skip to content

Instantly share code, notes, and snippets.

@meitinger
Last active April 22, 2020 13:21
Show Gist options
  • Save meitinger/6ff74ec1e0abbbeea6e4002d47b42fce to your computer and use it in GitHub Desktop.
Save meitinger/6ff74ec1e0abbbeea6e4002d47b42fce to your computer and use it in GitHub Desktop.
Utility that keeps track of your active logon time.
/*
* TimeTracker
* Copyright (C) 2020 Manuel Meitinger
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#nullable enable
using Microsoft.Win32;
using System;
using System.ComponentModel;
using System.Drawing;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows.Forms;
[assembly: AssemblyTitle("TimeTracker")]
[assembly: AssemblyDescription("Utility that keeps track of your active logon time.")]
[assembly: AssemblyCompany("AufBauWerk - Unternehmen für junge Menschen")]
[assembly: AssemblyProduct("TimeTracker")]
[assembly: AssemblyCopyright("Copyright © 2020 Manuel Meitinger")]
[assembly: ComVisible(false)]
[assembly: AssemblyVersion("1.0.0.2")]
namespace TimeTracker
{
public static class Program
{
private static class Win32
{
private const int MAX_PATH = 260;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct LASTINPUTINFO
{
public int cbSize;
public uint dwTime;
}
[Flags]
private enum SHGSI : uint
{
ICON = 0x000000100,
SHELLICONSIZE = 0x000000004
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct SHSTOCKICONINFO
{
public int cbSize;
public IntPtr hIcon;
public int iSysImageIndex;
public int iIcon;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_PATH)]
public string szPath;
}
private enum SIID { RENAME = 83 };
[DllImport("User32.dll", ExactSpelling = true, CharSet = CharSet.Unicode)]
private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
[DllImport("Kernel32.dll", ExactSpelling = true, CharSet = CharSet.Unicode)]
private static extern uint GetTickCount();
[DllImport("Shell32.dll", ExactSpelling = true, CharSet = CharSet.Unicode, PreserveSig = false)]
private static extern void SHGetStockIconInfo(SIID siid, SHGSI uFlags, ref SHSTOCKICONINFO psii);
private static DateTime _lastKnownInputTime = DateTime.Now;
public static DateTime GetLastInputTime()
{
var lastInputInfo = new LASTINPUTINFO() { cbSize = Marshal.SizeOf(typeof(LASTINPUTINFO)) };
if (GetLastInputInfo(ref lastInputInfo))
{
var currectTick = GetTickCount();
_lastKnownInputTime = DateTime.Now - TimeSpan.FromTicks(unchecked(currectTick - lastInputInfo.dwTime) * 10000L);
}
return _lastKnownInputTime;
}
public static Icon GetRenameIcon()
{
var shellIconResult = new SHSTOCKICONINFO { cbSize = Marshal.SizeOf(typeof(SHSTOCKICONINFO)) };
SHGetStockIconInfo(SIID.RENAME, SHGSI.ICON | SHGSI.SHELLICONSIZE, ref shellIconResult);
return Icon.FromHandle(shellIconResult.hIcon);
}
}
private static class Store
{
private const string RegBaseKey = @"HKEY_CURRENT_USER\Software\TimeTracker";
private static long GetTicks([CallerMemberName] string name = "") => Registry.GetValue(RegBaseKey, name, null) as long? ?? throw new ApplicationException($@"{new Win32Exception(1012).Message} ({RegBaseKey}\{name})");
private static void SetTicks(long ticks, [CallerMemberName] string name = "") => Registry.SetValue(RegBaseKey, name, ticks, RegistryValueKind.QWord);
public static TimeSpan DailyQuota => TimeSpan.FromTicks(GetTicks());
public static TimeSpan MaxIdle => TimeSpan.FromTicks(GetTicks());
public static DateTime StartDate => DateTime.FromBinary(GetTicks());
public static TimeSpan TotalWork
{
get => TimeSpan.FromTicks(GetTicks());
set => SetTicks(value.Ticks);
}
}
[STAThread]
public static void Main()
{
var components = (Container?)null;
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
{
components?.Dispose();
MessageBox.Show(e.ExceptionObject?.ToString(), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
Environment.Exit(e.ExceptionObject switch { Win32Exception ex => ex.NativeErrorCode, Exception ex => ex.HResult, _ => -1 });
};
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
using (CreateForm(components = new Container()))
{
Application.Run();
}
}
private static Form CreateForm(Container components)
{
//
// initialize
//
var form = new Form()
{
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
Icon = Win32.GetRenameIcon(),
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow,
ShowInTaskbar = false,
StartPosition = FormStartPosition.Manual,
TopMost = true,
};
form.Disposed += (_, e) => components.Dispose();
form.FormClosing += (_, e) =>
{
if (e.CloseReason == CloseReason.UserClosing)
{
e.Cancel = true;
form.Hide();
}
};
//
// notify icon
//
var notifyIcon = new System.Windows.Forms.NotifyIcon(components)
{
Icon = Win32.GetRenameIcon(),
Visible = true,
};
notifyIcon.Click += (_, e) =>
{
if (form.Visible)
{
form.Hide();
}
else
{
var position = Cursor.Position;
var workArea = Screen.GetWorkingArea(position);
position.X = FitToStrip(position.X, workArea.Left, workArea.Right, form.Width);
position.Y = FitToStrip(position.Y, workArea.Top, workArea.Bottom, form.Height);
form.Location = position;
form.Show();
form.Focus();
}
};
//
// timer
//
var lastTick = DateTime.Now;
var timer = new System.Windows.Forms.Timer(components)
{
Enabled = true,
Interval = 1000,
};
timer.Tick += (_, e) =>
{
var now = DateTime.Now;
var maxIdle = Store.MaxIdle;
if (now - Win32.GetLastInputTime() <= maxIdle)
{
var duration = now - lastTick;
if (duration > maxIdle)
{
duration = maxIdle;
}
Store.TotalWork += duration;
UpdateTexts();
}
lastTick = now;
};
//
// adjustment box
//
var adjustmentTextBox = new System.Windows.Forms.MaskedTextBox()
{
Margin = Padding.Empty,
Mask = "#00:00:00",
ValidatingType = typeof(TimeSpan),
};
adjustmentTextBox.KeyUp += (_, e) =>
{
if (e.KeyCode == Keys.Enter)
{
var validated = adjustmentTextBox.ValidateText();
if (validated != null)
{
Store.TotalWork += (TimeSpan)validated;
UpdateTexts();
adjustmentTextBox.SelectAll();
}
}
};
form.Controls.Add(adjustmentTextBox);
//
// finalize
//
UpdateTexts();
return form;
void UpdateTexts() => notifyIcon.Text = form.Text = (Store.TotalWork - TimeSpan.FromTicks((long)(DateTime.Today - Store.StartDate).TotalDays * Store.DailyQuota.Ticks)).ToString();
int FitToStrip(int pos, int min, int max, int length) =>
pos < min ? min :
pos > max ? max - length :
pos - length / 2;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment