Last active
April 22, 2020 13:21
-
-
Save meitinger/6ff74ec1e0abbbeea6e4002d47b42fce to your computer and use it in GitHub Desktop.
Utility that keeps track of your active logon time.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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