Initial commit of fresh repo 2024-06-25

This commit is contained in:
Trey Blancher 2024-06-25 14:55:05 -04:00
commit 03d47d8233
33 changed files with 3870 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Log files, and archives
*log
*.log
*.pyc
*.sw*
*.tar
*.xz
2017*
2018*
2019*
2020*
2021*
2022*
do_process.bak2017-11-06_file-test
rust/*/target/*
vim/syntax/README.md
vim/syntax/timetracker.vim.hide
vim/syntax/timetracker.vim.tmp
staging
test.py
test.sh
timetracker.py-pre-incident-fixup
toggle_remote.sh
work

108
.vimrc Executable file
View File

@ -0,0 +1,108 @@
set nomodeline
set t_Co=256
"set t_Co=16
set t_ut=
set ai
set background=dark
"set background=light
"set textwidth=80
set hlsearch
"color asu1dark
"set t_ut=
let g:solarized_italic=0
"let g:solarized_bold=0
"let g:solarized_underline=0
"colorscheme sorcerer
let g:csv_delim=','
" Pathogen
filetype off " Pathogen needs to run before plugin indent on
"execute pathogen#infect()
"call pathogen#runtime_append_all_bundles()
"call pathogen#helptags() " generate help tags for everything in 'runtimepath'
filetype plugin indent on
filetype plugin on
"colorscheme gruvbox
syntax on
set tabstop=4
set softtabstop=4
set shiftwidth=4
set expandtab
nnoremap <F2> :GundoToggle<CR>
nnoremap <C-j> <C-w>w
"imap  <End>
"imap  <Home>
"nnoremap  $
"nnoremap  <bar>
nmap Y y$
nmap C c$
"nnoremap V <C-v>
"inoremap jj <Esc>
"inoremap kk <Esc>
"inoremap hh <Esc>
"inoremap lll <Esc>
"inoremap bbb <Esc>
"inoremap eeee <Esc>
"inoremap ww <Esc>
"inoremap yy <Esc>yy
"inoremap ddd <Esc>dd
"inoremap dG <Esc>dG
"inoremap :w <Esc>:w
"inoremap :wq <Esc>:wq
"inoremap :q <Esc>:q
"inoremap :q! <Esc>:q!
"nnoremap <S-Tab> <<
"inoremap <S-Tab> <C-d>
" Force me to stop using arrow keys
"inoremap <Left> <Nop>
"nnoremap <Left> <Nop>
"inoremap <Right> <Nop>
"nnoremap <Right> <Nop>
"inoremap <Up> <Nop>
"nnoremap <Up> <Nop>
"inoremap <Down> <Nop>
"nnoremap <Down> <Nop>
" Fix Home/End/Delete
set backspace=indent,eol,start
"fixdel
" Idle timeout, exit insert mode after thirty seconds:
"au CursorHoldI * stopinsert
"set updatetime=30000
"if $TMUX == ''
" set clipboard+=unnamed
"endif
set clipboard+=unnamed
set laststatus=2
set statusline=[%n]\ %<%F\ \ \ [%M%R%H%W%Y][%{&ff}]\ \ %=\ line:%l/%L\ col:%c\ \ \ %p%%\ \ \ @%{strftime(\"%H:%M:%S\")}
if $VIM_CRONTAB == "true"
set nobackup
set nowritebackup
endif
"set spell spelllang=en
" Change cursor shape between insert and normal mode in iTerm2.app
if $TERM_PROGRAM =~ "iTerm.app"
if exists('$TMUX')
let &t_SI = "\<Esc>Ptmux;\<Esc>\<Esc>]50;CursorShape=1\x7\<Esc>\\" " Vertical bar in insert mode
let &t_EI = "\<Esc>Ptmux;\<Esc>\<Esc>]50;CursorShape=0\x7\<Esc>\\" " Block in normal mode
let &t_SR = "\<Esc>Ptmux;\<Esc>\<Esc>]50;CursorShape=2\x7\<Esc>\\" " Block in normal mode
else
let &t_SI = "\<Esc>]50;CursorShape=1\x7" " Vertical bar in insert mode
let &t_EI = "\<Esc>]50;CursorShape=0\x7" " Block in normal mode
let &t_SR = "\<Esc>]50;CursorShape=2\x7" " Block in normal mode
endif
endif
if has("autocmd")
au BufReadPost * if line("'\"") > 0 && line("'\"") <= line("$") | exe "normal! g`\"" | endif
endif
autocmd BufRead,BufNewFile ~/pindrop/meetings/cs_support_weekly/* source ~/.muttvimrc
autocmd BufRead,BufNewFile ~/pindrop/meetings/cs_support_proactive_monitoring/* source ~/.muttvimrc
autocmd BufRead,BufNewFile ~/meetings/* source ~/.muttvimrc

View File

@ -0,0 +1,596 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>Activate</key>
<string>Normal</string>
<key>CreationDate</key>
<real>475427766.25503999</real>
<key>Macros</key>
<array>
<dict>
<key>Actions</key>
<array>
<dict>
<key>DisplayKind</key>
<string>Variable</string>
<key>HonourFailureSettings</key>
<true/>
<key>IncludeStdErr</key>
<true/>
<key>MacroActionType</key>
<string>ExecuteShellScript</string>
<key>NotifyOnFailure</key>
<false/>
<key>Path</key>
<string></string>
<key>Source</key>
<string>Nothing</string>
<key>StopOnFailure</key>
<false/>
<key>Text</key>
<string>printf "%s" "$(date '+%F %T')"</string>
<key>TimeOutAbortsMacro</key>
<true/>
<key>TrimResults</key>
<false/>
<key>TrimResultsNew</key>
<false/>
<key>UseText</key>
<true/>
<key>Variable</key>
<string>DateStamp</string>
</dict>
<dict>
<key>DisplayKind</key>
<string>Variable</string>
<key>HonourFailureSettings</key>
<true/>
<key>IncludeStdErr</key>
<true/>
<key>MacroActionType</key>
<string>ExecuteShellScript</string>
<key>NotifyOnFailure</key>
<false/>
<key>Path</key>
<string></string>
<key>Source</key>
<string>Nothing</string>
<key>StopOnFailure</key>
<false/>
<key>Text</key>
<string>printf "%s" "$(date '+%F')"</string>
<key>TimeOutAbortsMacro</key>
<true/>
<key>TrimResults</key>
<false/>
<key>TrimResultsNew</key>
<false/>
<key>UseText</key>
<true/>
<key>Variable</key>
<string>TimeLogDate</string>
</dict>
<dict>
<key>Buttons</key>
<array>
<dict>
<key>Button</key>
<string>OK</string>
</dict>
<dict>
<key>Button</key>
<string>Cancel</string>
<key>Cancel</key>
<true/>
</dict>
</array>
<key>MacroActionType</key>
<string>PromptForUserInput</string>
<key>Prompt</key>
<string>[Customer] PDROP-TICKET Comment</string>
<key>TimeOutAbortsMacro</key>
<true/>
<key>Title</key>
<string>End Task</string>
<key>Variables</key>
<array>
<dict>
<key>Default</key>
<string>%Variable%DateStamp%</string>
<key>Variable</key>
<string>CurrentTaskDateStamp</string>
</dict>
</array>
</dict>
<dict>
<key>DisplayKind</key>
<string>Window</string>
<key>HonourFailureSettings</key>
<true/>
<key>IncludeStdErr</key>
<false/>
<key>MacroActionType</key>
<string>ExecuteShellScript</string>
<key>Path</key>
<string></string>
<key>Source</key>
<string>Nothing</string>
<key>Text</key>
<string>atom $KMVAR_TimeLogPath/$KMVAR_TimeLogDate.log</string>
<key>TimeOutAbortsMacro</key>
<true/>
<key>TrimResults</key>
<true/>
<key>TrimResultsNew</key>
<true/>
<key>UseText</key>
<true/>
</dict>
<dict>
<key>Conditions</key>
<dict>
<key>ConditionList</key>
<array>
<dict>
<key>Application</key>
<dict>
<key>BundleIdentifier</key>
<string>com.github.atom</string>
<key>Name</key>
<string>Atom</string>
<key>NewFile</key>
<string>/Applications/Atom.app</string>
</dict>
<key>ApplicationConditionType</key>
<string>Active</string>
<key>ConditionType</key>
<string>Application</string>
</dict>
</array>
<key>ConditionListMatch</key>
<string>All</string>
</dict>
<key>MacroActionType</key>
<string>PauseUntil</string>
<key>TimeOutAbortsMacro</key>
<true/>
</dict>
<dict>
<key>MacroActionType</key>
<string>Pause</string>
<key>Time</key>
<string>0.5</string>
<key>TimeOutAbortsMacro</key>
<true/>
</dict>
<dict>
<key>KeyCode</key>
<integer>125</integer>
<key>MacroActionType</key>
<string>SimulateKeystroke</string>
<key>Modifiers</key>
<integer>256</integer>
<key>ReleaseAll</key>
<false/>
<key>TargetApplication</key>
<dict/>
<key>TargetingType</key>
<string>Front</string>
</dict>
<dict>
<key>Action</key>
<string>ByTyping</string>
<key>MacroActionType</key>
<string>InsertText</string>
<key>TargetApplication</key>
<dict/>
<key>TargetingType</key>
<string>Front</string>
<key>Text</key>
<string>%Variable%CurrentTaskDateStamp%: End </string>
</dict>
<dict>
<key>Action</key>
<string>ByTyping</string>
<key>MacroActionType</key>
<string>InsertText</string>
<key>TargetApplication</key>
<dict/>
<key>TargetingType</key>
<string>Front</string>
<key>Text</key>
<string>%Variable%CurrentTask%</string>
</dict>
<dict>
<key>MacroActionType</key>
<string>Pause</string>
<key>Time</key>
<string>0.2</string>
<key>TimeOutAbortsMacro</key>
<true/>
</dict>
<dict>
<key>MacroActionType</key>
<string>DeletePastClipboard</string>
<key>PastExpression</key>
<string>0</string>
</dict>
<dict>
<key>MacroActionType</key>
<string>DeletePastClipboard</string>
<key>PastExpression</key>
<string>1</string>
</dict>
<dict>
<key>KeyCode</key>
<integer>1</integer>
<key>MacroActionType</key>
<string>SimulateKeystroke</string>
<key>Modifiers</key>
<integer>256</integer>
<key>ReleaseAll</key>
<false/>
<key>TargetApplication</key>
<dict/>
<key>TargetingType</key>
<string>Front</string>
</dict>
</array>
<key>CreationDate</key>
<real>608055049.31738698</real>
<key>ModificationDate</key>
<real>608064257.03387797</real>
<key>Name</key>
<string>Insert Timestamp End (Atom)</string>
<key>Triggers</key>
<array>
<dict>
<key>FireType</key>
<string>Pressed</string>
<key>KeyCode</key>
<integer>2</integer>
<key>MacroTriggerType</key>
<string>HotKey</string>
<key>Modifiers</key>
<integer>768</integer>
</dict>
<dict>
<key>ElementCookie</key>
<integer>186</integer>
<key>ElementDeviceUsage</key>
<integer>6</integer>
<key>ElementDeviceUsagePage</key>
<integer>1</integer>
<key>ElementName</key>
<string>SteelSeries Apex Gaming Keyboard #169</string>
<key>ElementProductID</key>
<integer>4610</integer>
<key>ElementShortName</key>
<string>#169</string>
<key>ElementUsage</key>
<integer>169</integer>
<key>ElementUsagePage</key>
<integer>7</integer>
<key>ElementValue</key>
<integer>1</integer>
<key>ElementVendorID</key>
<integer>4152</integer>
<key>FireType</key>
<string>Pressed</string>
<key>MacroTriggerType</key>
<string>HID</string>
<key>Modifiers</key>
<integer>0</integer>
<key>UseModifiers</key>
<true/>
</dict>
</array>
<key>UID</key>
<string>C51D7A8E-2D00-43D0-8FC5-8C2C89E0E2FA</string>
</dict>
</array>
<key>Name</key>
<string>Global Macro Group</string>
<key>ToggleMacroUID</key>
<string>68B1DBE3-6AF3-48B7-8AC6-B33CF06FE6C3</string>
<key>UID</key>
<string>F9C097B6-B2EC-453F-A623-9EDD70926EC8</string>
</dict>
<dict>
<key>Activate</key>
<string>Normal</string>
<key>CreationDate</key>
<real>475427766.25503999</real>
<key>Macros</key>
<array>
<dict>
<key>Actions</key>
<array>
<dict>
<key>DisplayKind</key>
<string>Variable</string>
<key>HonourFailureSettings</key>
<true/>
<key>IncludeStdErr</key>
<true/>
<key>MacroActionType</key>
<string>ExecuteShellScript</string>
<key>NotifyOnFailure</key>
<false/>
<key>Path</key>
<string></string>
<key>Source</key>
<string>Nothing</string>
<key>StopOnFailure</key>
<false/>
<key>Text</key>
<string>printf "%s" "$(date '+%F %T')"</string>
<key>TimeOutAbortsMacro</key>
<true/>
<key>TrimResults</key>
<false/>
<key>TrimResultsNew</key>
<false/>
<key>UseText</key>
<true/>
<key>Variable</key>
<string>DateStamp</string>
</dict>
<dict>
<key>DisplayKind</key>
<string>Variable</string>
<key>HonourFailureSettings</key>
<true/>
<key>IncludeStdErr</key>
<true/>
<key>MacroActionType</key>
<string>ExecuteShellScript</string>
<key>NotifyOnFailure</key>
<false/>
<key>Path</key>
<string></string>
<key>Source</key>
<string>Nothing</string>
<key>StopOnFailure</key>
<false/>
<key>Text</key>
<string>printf "%s" "$(date '+%F')"</string>
<key>TimeOutAbortsMacro</key>
<true/>
<key>TrimResults</key>
<false/>
<key>TrimResultsNew</key>
<false/>
<key>UseText</key>
<true/>
<key>Variable</key>
<string>TimeLogDate</string>
</dict>
<dict>
<key>Buttons</key>
<array>
<dict>
<key>Button</key>
<string>OK</string>
</dict>
<dict>
<key>Button</key>
<string>Cancel</string>
<key>Cancel</key>
<true/>
</dict>
</array>
<key>MacroActionType</key>
<string>PromptForUserInput</string>
<key>Prompt</key>
<string>[Customer] PDROP-TICKET Comment</string>
<key>TimeOutAbortsMacro</key>
<true/>
<key>Title</key>
<string>Begin Taks</string>
<key>Variables</key>
<array>
<dict>
<key>Default</key>
<string></string>
<key>Variable</key>
<string>CurrentTask</string>
</dict>
<dict>
<key>Default</key>
<string>%Variable%DateStamp%</string>
<key>Variable</key>
<string>CurrentTaskDateStamp</string>
</dict>
</array>
</dict>
<dict>
<key>DisplayKind</key>
<string>Window</string>
<key>HonourFailureSettings</key>
<true/>
<key>IncludeStdErr</key>
<false/>
<key>MacroActionType</key>
<string>ExecuteShellScript</string>
<key>Path</key>
<string></string>
<key>Source</key>
<string>Nothing</string>
<key>Text</key>
<string>atom $KMVAR_TimeLogPath/$KMVAR_TimeLogDate.log</string>
<key>TimeOutAbortsMacro</key>
<true/>
<key>TrimResults</key>
<true/>
<key>TrimResultsNew</key>
<true/>
<key>UseText</key>
<true/>
</dict>
<dict>
<key>Conditions</key>
<dict>
<key>ConditionList</key>
<array>
<dict>
<key>Application</key>
<dict>
<key>BundleIdentifier</key>
<string>com.github.atom</string>
<key>Name</key>
<string>Atom</string>
<key>NewFile</key>
<string>/Applications/Atom.app</string>
</dict>
<key>ApplicationConditionType</key>
<string>Active</string>
<key>ConditionType</key>
<string>Application</string>
</dict>
</array>
<key>ConditionListMatch</key>
<string>All</string>
</dict>
<key>MacroActionType</key>
<string>PauseUntil</string>
<key>TimeOutAbortsMacro</key>
<true/>
</dict>
<dict>
<key>MacroActionType</key>
<string>Pause</string>
<key>Time</key>
<string>0.5</string>
<key>TimeOutAbortsMacro</key>
<true/>
</dict>
<dict>
<key>KeyCode</key>
<integer>125</integer>
<key>MacroActionType</key>
<string>SimulateKeystroke</string>
<key>Modifiers</key>
<integer>256</integer>
<key>ReleaseAll</key>
<false/>
<key>TargetApplication</key>
<dict/>
<key>TargetingType</key>
<string>Front</string>
</dict>
<dict>
<key>Action</key>
<string>ByTyping</string>
<key>MacroActionType</key>
<string>InsertText</string>
<key>TargetApplication</key>
<dict/>
<key>TargetingType</key>
<string>Front</string>
<key>Text</key>
<string>%Variable%CurrentTaskDateStamp%: Begin </string>
</dict>
<dict>
<key>Action</key>
<string>ByTyping</string>
<key>MacroActionType</key>
<string>InsertText</string>
<key>TargetApplication</key>
<dict/>
<key>TargetingType</key>
<string>Front</string>
<key>Text</key>
<string>%Variable%CurrentTask%</string>
</dict>
<dict>
<key>KeyCode</key>
<integer>1</integer>
<key>MacroActionType</key>
<string>SimulateKeystroke</string>
<key>Modifiers</key>
<integer>256</integer>
<key>ReleaseAll</key>
<false/>
<key>TargetApplication</key>
<dict/>
<key>TargetingType</key>
<string>Front</string>
</dict>
<dict>
<key>MacroActionType</key>
<string>DeletePastClipboard</string>
<key>PastExpression</key>
<string>0</string>
</dict>
<dict>
<key>MacroActionType</key>
<string>DeletePastClipboard</string>
<key>PastExpression</key>
<string>1</string>
</dict>
</array>
<key>CreationDate</key>
<real>608055033.23224998</real>
<key>ModificationDate</key>
<real>608064243.31968701</real>
<key>Name</key>
<string>Insert Timestamp Begin (Atom)</string>
<key>Triggers</key>
<array>
<dict>
<key>FireType</key>
<string>Pressed</string>
<key>KeyCode</key>
<integer>2</integer>
<key>MacroTriggerType</key>
<string>HotKey</string>
<key>Modifiers</key>
<integer>768</integer>
</dict>
<dict>
<key>ElementCookie</key>
<integer>185</integer>
<key>ElementDeviceUsage</key>
<integer>6</integer>
<key>ElementDeviceUsagePage</key>
<integer>1</integer>
<key>ElementName</key>
<string>SteelSeries Apex Gaming Keyboard #168</string>
<key>ElementProductID</key>
<integer>4610</integer>
<key>ElementShortName</key>
<string>#168</string>
<key>ElementUsage</key>
<integer>168</integer>
<key>ElementUsagePage</key>
<integer>7</integer>
<key>ElementValue</key>
<integer>1</integer>
<key>ElementVendorID</key>
<integer>4152</integer>
<key>FireType</key>
<string>Pressed</string>
<key>MacroTriggerType</key>
<string>HID</string>
<key>Modifiers</key>
<integer>0</integer>
<key>UseModifiers</key>
<true/>
</dict>
</array>
<key>UID</key>
<string>AC01276C-6D6B-4C78-98CB-9D68F703E740</string>
</dict>
</array>
<key>Name</key>
<string>Global Macro Group</string>
<key>ToggleMacroUID</key>
<string>68B1DBE3-6AF3-48B7-8AC6-B33CF06FE6C3</string>
<key>UID</key>
<string>F9C097B6-B2EC-453F-A623-9EDD70926EC8</string>
</dict>
</array>
</plist>

70
INSTALL.md Normal file
View File

@ -0,0 +1,70 @@
# timetracker INSTALL
## Basic Installation
Use `git clone` to create a local copy of the timetracker repository on your local MacBook Pro. If you'd rather use the older, slower Python/Bash scripts, you can then copy or link the `timetracker.py`, `do_process.sh`, and `chug.sh` files into your path. I like to keep these in a `~/timetracker` directory, where I execute them with a leading `./` so they don't need to be in my `PATH` variable.
If you'd prefer to use the Rust programs (which is highly recommended, if only for execution speed), you will need to compile the Rust programs with `cargo` (the Rust language package manager). Do the following:
1. Install the `rust` package from Homebrew:
```
brew install rust
```
Homebrew may update itself before installing `rust`.
1. Navigate to the `timetracker/rust/timetracking` directory in the cloned `timetracker` source directory
1. Execute the following command:
```
cargo build --release
```
If you are intending to debug or otherwise develop the Rust programs, I recommend creating a fork on GitHub, making your proposed changes in your local clone of the fork, and then issue a PR against my parent repository. In that case you can drop the `--release` argument, which will keep debugging symbols and such in the executable.
1. The compiled executables will be in the `target/release` or the `target/debug` subdirectories, depending on whether you provided `--release` to the build step (without this flag the executables will be in the `debug` subdirectory). You may copy them or link them to your `PATH` (e.g., `~/bin`) for use.
- Note that if you're working on a specific program in the package, you can change directory to whichever executable you're working on (`chug`, `doprocess`, `timetracker`, or the library package `timelogging`), you will likely want to run the debug version directly, like so:
```
cargo run -- <input arguments>
```
## Pro Tips
There are some things that you need to do in order to use timetracker like a pro. These instructions assume you're using the vim or gvim (GUI) editor. There are likely similar macros you can enable for editors such as Atom or Sublime, but you'll have to review the documentation for those editors to be able to reproduce the ideas there.
Here are the basics:
1. Install Keyboard Maestro from the MSC. If it is not available to you, send a helpdesk request to helpdesk@pindrop.com to request it. Install it using the standard MSC method.
1. Launch Keyboard Maestro. Two pieces should launch, the Editor and the Engine. If the Engine is loaded you should see the ![Keyboard Maestro icon][KM_icon] in your toolbar.
1. In the Keyboard Maestro Editor, select ![File/Import Macros Safely...][import_menu]. Import the `timetracker.kmmacros` file into Keyboard Maestro. All imported macros will be disabled by default.
1. Enable the "Insert Timestamp Begin" and "Insert Timestamp End" macros: ![Begin/End][enable_macros]. To select multiple macros hold down the "Command" ('⌘') and click each desired macro. Then click "Enable or Disable Macro" at the bottom of the Keyboard Maestro Editor window, in the center pane under "Macros".
* If desired, you can modify the trigger for these macros at this time: ![Change Trigger][change_trigger]. You'll have to do this for each macro individually.
* As imported, the Begin and End macros are triggered by: ⌘+Shift+D
1. You can repeat the above process for the "Insert Date (ISO 8601 format)" macro, which will allow you to quickly type out today's date in ISO-8601 date standard formate (e.g., "2020-04-07").
1. Now that you have the basic Begin and End macros imported and enabled, you can begin using them. In your favorite terminal emulator (e.g. iTerm2 or Terminal.app), open vim on today's log file:
```
cd ~/timetracker
vim 2020-04-07.log
```
Or, if you prefer, use gvim or Mac vim GUI programs.
1. If you haven't already, copy the .vimrc file from the timetracker GitHub repository to your home directory (`/Users/<username>/`, or `~/`). If you already have a .vimrc file that has your own customizations in it, consider adding the following line to your .vimrc:
```
nmap Y y$
```
If you do not do this you will have to modify the vim macro (see below).
1. The Keyboard Maestro macros make use of the `@a` vim macro. To record it in vim:
* Press `q`, then release, and then type `a`. You should now see `recording @a` in the vim status line at the bottom of your vim window.
* Press Escape (`Esc`) to go into normal/command vim mode (where you can navigate with `hjkl` keys).
* If you are using the timetracker .vimrc, or have made the suggested edit to your own .vimrc, type the following keys:
```
klYjp
```
* If you are not using the timetracker .vimrc, and have not made the suggested edit to your own .vimrc, type the following commands instead:
```
kly$jp
```
* Press `q` again to stop recording the macro.
* Trigger the vim macro by typing `@a` (the "Insert Timestamp End" macro does this automatically)
What these vim macros do is copy the category and task name from the above line (should be a Begin task), and paste it to the end of the End line. This should eliminate any mistakes when setting the end time of a task. Note that if a mistake is made on the Begin line, that mistake will be propagated to the End line when the "Insert Timestampt End" macro is executed.
1. Both the "Insert Timestamp Begin" and "Insert Timestamp End" macros automatically press the Escape key (`Esc`), so you can trigger them from normal/command mode, or insert mode. In vim, trigger the "Insert Timestamp Begin" macro by typing `⌘+Shift+D`, then `B` (or whatever you changed the trigger to be). If you haven't changed the trigger, a ![Conflict Dialog][conflict_dialog] will pop up, where you can choose the desired macro (either with your mouse, or by typing `B` for Begin, or `E` for End).
1. If you have chosen the "Insert Timestamp Begin" macro, you can type a category (`[In Square Brackets]`), then the title of the task you are beginning. Note that the title is free form, but having square brackets in the title, outside of the category is untested. Any other characters should be valid.
- NOTE: If you use the Rust programs for digesting your logs, having multiple categories/strings inside square brackets will cause the Rust program to panic. The older Python/Bash scripts handle this a bit more gracefully at the time of this writing.
1. When you're finished with the task, run the "Insert Timestamp End" macro. You should see the timestamp with the "End" keyword, and the category and title of the above Begin task copied verbatim. Then the macro will automatically print another Begin line, ready for the next task.
Reach out to me (Trey Blancher, tblancher@pindrop.com), if you have questions or problems with this procedure. Or better yet, file an issue under the timetracker repository on GitHub.
[KM_icon]: https://github.atl.pdrop.net/tblancher/timetracker/tree/master/images/KM_icon.png
[import_menu]: https://github.atl.pdrop.net/tblancher/timetracker/tree/master/images/import_menu.png
[enable_macros]: https://github.atl.pdrop.net/tblancher/timetracker/tree/master/images/enable_macros.png
[change_trigger]: https://github.atl.pdrop.net/tblancher/timetracker/tree/master/images/change_trigger.png
[conflict_dialog]: https://github.atl.pdrop.net/tblancher/timetracker/tree/master/images/conflict_dialog.png

307
README.md Normal file
View File

@ -0,0 +1,307 @@
# timetracker
tblancher's CLI-based time tracking system (see demo videos of the [timetracker setup](https://pindrop.zoom.us/rec/share/wvN6FoPfyEhJfo3x12qFVKofHL30X6a81Sce__IJmEp90tBtwdKHn7JnxZMQfz69), [macro setup](https://pindrop.zoom.us/rec/share/w-lYBpf2q0hLYpHR-n7bY6gwMr_Caaa81ClK__YJmR2E6NY1ybj-wurKfyog0FpO), and [Clarizen process](https://pindrop.zoom.us/rec/share/yuF4C7_7rFlJAYnAq2TwarETAorMeaa8gXQdqfUPnRsgkXJf9_ByKUpI8hnGW1Jp))
## INSTALLATION
See [INSTALL.md](https://github.atl.pdrop.net/tblancher/timetracker/blob/master/INSTALL.md) in this repository.
## OVERVIEW
This framework is what I use to keep track of my time, as I work. Only one task is allowed concurrently, so if I switch to another task, I must end the current task before proceeding. This is tracked in a log file with the following format:
## log file format
```
2017-12-08 08:21:27: Begin [Internal] email triage
2017-12-08 08:59:05: End [Internal] email triage
2017-12-08 08:59:06: Begin [Internal] internal task 1
2017-12-08 09:10:55: End [Internal] internal task 1
2017-12-08 09:10:56: Begin [Internal] internal task 1
2017-12-08 09:15:14: End [Internal] internal task 1
2017-12-08 09:15:14: Begin [Cloud] Customer 1 task 1
2017-12-08 09:19:52: End [Cloud] Customer 1 task 1
2017-12-08 09:19:52: Begin [Cloud] Customer 1 PDROP-0000000 case-related task 1
2017-12-08 09:27:37: End [Cloud] Customer 1 PDROP-0000000 case-related task 1
2017-12-08 09:27:38: Begin [Cloud] Customer 2 task 2
2017-12-08 11:00:39: End [Cloud] Customer 2 task 2
2017-12-08 11:00:40: Begin [On Prem] Customer 1 PDROP-0000000 case-related task 2
2017-12-08 11:07:24: End [On Prem] Customer 1 PDROP-0000000 case-related task 2
2017-12-08 11:08:56: Begin [Internal] internal task 2
2017-12-08 11:25:43: End [Internal] internal task 2
2017-12-08 12:47:50: Begin [Cloud] Customer 1 PDROP-0000000 case-related task 1
2017-12-08 13:31:52: End [Cloud] Customer 1 PDROP-0000000 case-related task 1
2017-12-08 13:31:52: Begin [On Prem] Customer 3 external meeting
2017-12-08 14:11:13: End [On Prem] Customer 3 external meeting
2017-12-09 14:11:14: Begin [Internal] documentation creation
2017-12-09 17:01:47: End [Internal] documentation creation
```
The timestamp is naive, it is assumed to be in your local timezone. It is also in most-to-least significant units, though any valid date and naive time should work (however, any other format is untested. BEWARE!, also, the Rust programs will not work with any other format). The separator between the timestamp and the Begin or End keywoards is `: `, literally a colon followed by **two** spaces. Anything else will break timetracker.py (and the Rust version), and the rest of it will fall down. Each task must have a `Begin` and `End` line, or else the output of timetracker will not be correct. Too many begins or ends should be detected by timetracker.py, but this will likely cause a panic in the Rust programs. The name of the task is free form, and must be identical for the Begin and End (otherwise timetracker.py will think they're different tasks).
Certain strings have special meaning to timetracker. The category name in square brackets specifies that the task is related to a specific customer, or should be tracked on a certain item in Clarizen, to make transferring to Clarizen more straightforward. These can be any string, even with spaces, but beware of using shell special characters, or special regular expression characters. The names can be anything, so things like `[BB&T]`, `[Citi]`, `[Wells Fargo]`, or `[Internal]` are perfectly valid. For the author's most common categories, macros are set up with text string triggers in Keyboard Maestro (more on that below). If you decide to use `timetracker.py` (the Python 3 script), the category (in square-brackets) is optional, but won't have a separate section in `do_process.sh` (though it will reflect in the grand total at the end). If you decide to use the Rust programs, the category is **required**, or else the Rust programs will panic and error out. It is highly recommended to use the Rust programs, for speed if nothing else.
Note that the aggregate programs (`do_process.sh`, `doprocess` [Rust], `chug.sh`, `chug` [Rust]) expect the log file to be named by the ISO-8601 date format with the `.log` filename extension (e.g. `2017-06-09.log`, `2022-07-12.log`, `2022-12-19.log`), so it makes sense to keep only a single day's logs in one log file (since we're expected to load these times into Clarizen on a daily basis). However, there is nothing else in the code that makes this requirement (either Rust or Python/Bash scripts). In that case the aggregate programs will cover more than a day's worth of logs, which might make it difficult to transfer to Clarizen. Thus it is highly recommended to keep only a day's worth of logs in one log file. I recommend that if a task extends past midnight the next day, to End it at midnight in yesterday's log, and Begin it at midnight the next day's log.
An important point about this timetracker system: it automatically rounds the duration to the nearest quarter hour (nearest 15 minutes). If you document a task, but it takes less than 7.5 minutes from Begin to End, it's set to 0.08 hours (4 mintues, 48 seconds/288 seconds). Otherwise it would round down to 0.00hrs. If you don't want to count tasks that take so little time, you may want to remove them from the log. Previous versions of this system would round up to the nearest quarter hour, but I feared that was incorrectly inflating the amount of time I was spending during my day.
## timetracker in Rust
Again, see the [INSTALL.md](https://github.atl.pdrop.net/tblancher/timetracker/blob/master/INSTALL.md) guide in this repository for instructions on building the Rust programs. It is highly recommended to use the Rust programs instead of the older Python and Bash scripts below, if only because the Rust programs are much, much faster to execute than the older scripts. The usage is straightforward:
### timetracker usage
Pass the log name as the only argument to `timetracker`. Alternatively, you can execute a command pipeline that generates valid log output in the format above, piping it into timetracker:
```
timetracker 2022-12-19.log
grep 'Customer Name' 2022-12-19.log | timetracker
```
This will generate output similar to the following (using the above log file example as input):
```
[Cloud] Customer 1 PDROP-0000000 case-related task 1 0.75hrs
[Cloud] Customer 1 task 1 0.08hrs
[Cloud] Customer 2 task 2 1.50hrs
[Internal] documentation creation 2.75hrs
[Internal] email triage 0.75hrs
[Internal] internal task 1 0.25hrs
[Internal] internal task 2 0.25hrs
[On Prem] Customer 1 PDROP-0000000 case-related task 2 0.08hrs
[On Prem] Customer 3 external meeting 0.75hrs
Grand total: 7.16
```
This is the only Rust program that does not necessarily assume the log will be named in the ISO-8601 date format with `.log` filename extension (e.g. `2022-11-30.log`, `2022-12-19.log`, etc.). You can run this periodically throughout the day as you build the log file to see how much time you've put in thus far.
### doprocess usage
`doprocess` relies on the same internal Rust functions as `timetracker`, with the added output feature of separating the various categories into their own sections, for even easier transfer to Clarizen. With the new Clarizen categories, this makes entry into Clarizen take five minutes or less. You may want to sort your categories in Clarizen, to match the sort out of `doprocess`. Each section has its own total, with a running subtotal you can use to confirm you haven't missed anything as you transfer them to Clarizen. Its output looks like this:
```
[Cloud] Customer 1 PDROP-0000000 case-related task 1 0.75hrs
[Cloud] Customer 1 task 1 0.08hrs
[Cloud] Customer 2 task 2 1.50hrs
Section total: 2.33hrs
Subtotal: 2.33hrs
[Internal] documentation creation 2.75hrs
[Internal] email triage 0.75hrs
[Internal] internal task 1 0.25hrs
[Internal] internal task 2 0.25hrs
Section total: 4.00hrs
Subtotal: 6.33hrs
[On Prem] Customer 1 PDROP-0000000 case-related task 2 0.08hrs
[On Prem] Customer 3 external meeting 0.75hrs
Section total: 0.83hrs
Subtotal: 7.16hrs
Grand total: 7.16
```
For everything but the [Internal] category, you should be able to copy the entire section with Section total to a single line item in Clarizen. The Section total and Subtotal for the [Internal] category can be input as separate line items for each entry, and these totals help ensure you haven't missed one or input its duration improperly.
### chug usage
`chug` does not take a filename, or a log in the above format. Instead, it looks at the current week, and calculates the grand total for each day. If any day is missing, because the log file doesn't exist, it will print 0.00 for that day, like so:
```
2022-12-19.log
8.38
2022-12-20.log
0.00
2022-12-21.log
0.00
2022-12-22.log
0.00
2022-12-23.log
0.00
2022-12-24.log
0.00
2022-12-25.log
0.00
Grand total: 8.38
```
You can use this to review Clarizen, and make sure all time is input correctly. If you'd like to look at a previous week, you can pass the optional argument `1` or `+1`, `n` or `+n` where `n` is an unsigned integer for even further back. If you will be out of office on extended leave for the next week, and wish to run `chug` for it, pass a negative integer (e.g. `-1`, or `-n`). The formula for the week `chug` will look at is as follows:
```
<last Monday> - <week offset>
# where if offset is positive (either with or without a leading '+', it will look back). If the offset is negative it will look forward
# <last Monday> will be TODAY if today is Monday
```
## Caveats about Rust programs
The Rust programs have similar designs to the older Python and Bash scripts, but Rust is less forgiving regarding errors in the supplied logs (see format above). Namely, if you have log entries without a category, or try to use more than one category (`[In Square Brackets]`) in a single entry, Rust `timetracker`, `doprocess`, and `chug` programs will panic and error out. They should give a hint as to why they panic, but there is automatic Rust output that may not make it obvious what the problem is. Due to a recent update to the timelogging library code, too many Begin or End timestamps should be correctly identified. The programs still panic, but they give you an indication on what the problem is. For instance, with too many Begin timestamps, you'll see an error like the following:
```
thread 'main' panicked at 'ERROR: Missing more than one End', timelogging/src/lib.rs:97:21
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
Similarly, if you have too many End timestamps, you'll get an error that looks like this:
```
thread 'main' panicked at 'ERROR: Missing a Begin', timelogging/src/lib.rs:101:21
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
For correct operation, there should be an equal number of Begin and End timestamps. If you're in the middle of a task, the last End is optional, if it's the first entry of that task it will be the minimum duration (0.08 hours).
# Older Python and Bash scripts, for comparison
## timetracker.py usage
The timetracker.py script is the basis, it tallies the time for each task and then outputs a report on how much each task takes to complete. It takes as its argument one or more text files, the log files in the format above. It should only report one line item per task, regardless of how many times it appears in the log. Each task should have a category/tag in square brackets, the behavior of `do_process.sh` (see below) is now undefined if the tag is left out.
Note that the time gets rounded to the nearest quarter hour. If it would round to zero hours (0.00hrs), the script replaces it 0.08hrs, which corresponds to 288 seconds. If you don't want a task to be set to 0.08hrs, simply leave it out of the log. Prior to the merge of nearest-quarter-hour, everything was rounded UP to the nearest quarter hour. That meant that if a task took 30 minutes and one second, it would be rounded UP to 0.75hrs (45 minutes). On longer days this had the effect of inflating the daily total, such that 10 hours of actual work was being inflated to 12 or even 13 hours on some occasions. Now that it rounds up or down to the nearest quarter hour, timetracker.py is much more accurate. There still is a bit of inflation for the short tasks (anything taking less than 288 seconds), but it shouldn't be as bad as adding an extra 15 minutes to tasks that are close to quarter hour boundaries.
The output looks like this:
### timetracker output
```
[Internal] documentation creation 3.00hrs
[Internal] internal task 1 0.25hrs
[Internal] internal task 2 0.50hrs
[Cloud] Customer 1 PDROP-0000000 case-related task 1 1.00hrs
[Cloud] Customer 1 PDROP-0000000 case-related task 2 0.25hrs
[Cloud] Customer 1 task 1 0.08hrs
[Cloud] Customer 2 task 2 1.75hrs
[Cloud] Customer 3 external meeting 0.75hrs
[Internal] email triage 0.75hrs
[Internal] internal task 1 0.08hrs
Section total: 8.41hrs
```
You may have noticed, the output of timetracker.py is in alphabetical order by category, then by task. This follows for `./do_process.sh` and `./chug.sh` below.
## `do_process.sh`
This script filters the output of timetracker.py, giving each category (in square brackets) its own section, with its own tallies. The header is the list of categories and the log file basename (below this is `example.log`, but would normally be a date such as `2017-12-08.log`). After the header, each line of output and sections are designed to be directly transferred to Clarizen manually, and the tally used to verify the daily and weekly totals in Clarizen. It calculates the time total for each organization/category. It also prints the subtotal thus far, to be sure no items are missed when adding them to Clarizen. At the end it prints a grand total for the day, which should be used along with the subtotals to cross-verify in Clarizen.
```
[Internal]
[Cloud] Customer 1
[Cloud] Customer 2
[Cloud] Customer 3
[Internal]
--
example
[Internal] documentation creation 3.00hrs
[Internal] internal task 1 0.25hrs
[Internal] internal task 2 0.50hrs
Section total: 3.75hrs
Subtotal: 3.75hrs
[Cloud] Customer 1 PDROP-0000000 case-related task 1 1.00hrs
[Cloud] Customer 1 PDROP-0000000 case-related task 2 0.25hrs
[Cloud] Customer 1 task 1 0.08hrs
Section total: 1.33hrs
Subtotal: 5.08hrs
[Cloud] Customer 2 task 2 1.75hrs
Section total: 1.75hrs
Subtotal: 6.83
[Cloud] Customer 3 external meeting 0.75hrs
Section total: 0.75hrs
Subtotal: 7.58
[Internal] email triage 0.75hrs
[Internal] internal task 1 0.08hrs
Section total: 0.83hrs
Subtotal: 8.41hrs
Grand total: 8.41hrs
```
It takes as its argument a filename with the current date log (`date +%F` format, such as `./do_process.sh 2020-03-12.log`), or it assumes the current date log file. Also, arbitrary filenames can be passed, so `example.log` becomes `./do_process.sh example.log`.
Projects in Clarizen are listed alphabetically, so to transfer data from the `./do_process.sh` output you simply copy the data lines from the ouput, and paste it into the notes section of the Clarizen entry. Enter the duration of the task at the top of the Clarizen entry, and select the category and subcategory of the entry.
## `chug.sh`
This script is designed to be run on Mondays, after the previous week of log files have been generated and closed out. The standard Monday usage takes no arguments, it expects all log files to be processed to be in the current directory. It runs `do_process.sh` once for each day of the previous week, cleanly skipping any log files which do not exist. It pauses after each day report is output, allowing the user to transfer the times manually to Clarizen.
`./chug.sh` takes a single optional argument, a week offset (in case `./chug.sh` is executed for log files further back than last week). This uses the GNU date functionality of calculating "last Monday." On Monday this will be "today - 7 days", but on the following Tuesday this will evaluate to "yesterday". If Monday is a holiday and you're entering your timesheets on Tuesday you can enter `./chug.sh 1` and it should do the right thing. If it's the first Monday of the month and you need to process the previous four weeks of logs, use `./chug.sh 4`. The output of `./do_process.sh` prints the date that is being processed at the top, if your incantation of `./chug.sh` is wrong, you can quit and adjust accordingly. `./chug.sh` with no arguments is equivalent to `./chug.sh 0`.
The output is the output of `./do_process.sh` piped to `less` for each day, pausing so the user can go through that day's output and transfer the items to Clarizen. If no time was logged for a given day (the file does not exist), `./chug.sh` prints the missing date, but otherwise silently skips it. All seven days of the week are processed, Monday through Sunday.
If you're only interested in the weekly summary (and don't want the output of `./do_process.sh <date>.log | less`), you can pass the `-i` option to `./chug.sh`, like so: `./chug -i`, or if you need a prior week, e.g. `./chug -i 2`. The output will look similar to the following:
```
2020-03-16
9.05
2020-03-17
10.90
2020-03-18
11.96
2020-03-19
7.50
2020-03-20
9.38
2020-03-21
0
2020-03-22
0
Weekly Total: 48.79
```
## month-pack.sh
timetracker.py and the related `do_process.sh` and `chug.sh` scripts are designed to have each day with its own YYYY-MM-DD.log file in the current, timetracker directory. Over time, the log files in this directory can become quite numerous and unwieldy. To help combat this, `month-pack.sh` takes all the log files from the previous month, adds them to a compressed tarball, and deletes them from the directory. It is designed to be run once all of the log files for the previous month have been processed into Clarizen.
## year-pack.sh
In the same vein as month-pack.sh, year-pack.sh tars up all the monthly tarballs (named YYYY-MM.tar.xz), and puts them into a single YYYY.tar file. It is designed to be run in January when all of the previous December log files have been processed. All of the YYYY-MM.tar.xz files will be deleted once the YYYY.tar file is created.
# CAVEATS
## Tallies
Since I (the author, Trey Blancher) have been using this system to keep track of time, I've noticed that sometimes either Clarizen or these scripts get slightly off. Usually it's no more than 0.25 hours off in the tallies, but it gets time consuming trying to track down where the tally went wrong. If I do find the culprit, it's usually because I've entered the wrong time for a specific task in Clarizen (i.e., entering '0.08' instead of '0.25' for some tasks), or I've entered the time in the wrong cell.
### 2020-09-16 UPDATE
As of the institution of the running subtotals, the tallies being off in Clarizen is a *MUCH* less frequent problem. Usually if Clarizen is off it's because I missed an entry, or Clarizen didn't absorb an entry properly (that happens from time to time).
## vim
The vim-specific files in this repository are tailored for my tastes. One big item of note, I've disabled vim cursor navigation with the arrow keys (Up, Down, Left, Right), to force me to get into the habit of using h, j, k, l for cursor navigation, and only navigate in normal mode (not insert mode). You will probably want to delete the following lines from .vimrc if you're not interested in the true Vim way®:
```
" Force me to stop using arrow keys
inoremap <Left> <Nop>
nnoremap <Left> <Nop>
inoremap <Right> <Nop>
nnoremap <Right> <Nop>
inoremap <Up> <Nop>
nnoremap <Up> <Nop>
inoremap <Down> <Nop>
nnoremap <Down> <Nop>
```
My Keyboard Maestro macros for timetracker make use of specific vim macros, and certain keybindings within .vimrc. The file timetracker.vim contains a couple of alternate macros for this purpose.
Several of the vim shortcuts I've programmed assist in managing the log text file. Invariably there will come a time where I forget to document a few tasks right away, so I piece together the logs from Slack and email, and edit the logs so it matches the format. One notable "normal" mode mapping is 'Y', which copies from the cursor position to the end of the line ('yanks' from the current position to the EOL, similar to 'C' or 'D' for changing or deleting to the EOL). It is equivalent to the action 'y$', but is only one keystroke instead of two. The Keyboard Maestro macro for 'End' tasks uses this mapping heavily.
## Keyboard Maestro
Much of creating the log entries depends on specific Keyboard Maestro macros the author has set up. The file `timetracker.kmmacros` contains all of the macros used for this. The same text string trigger (Command + Shift + D, but you can assign it to any trigger) brings up a menu (known as a "conflict dialog" in KM), where I select '*B*egin' or '*E*nd'. The 'Begin' macro prints a log line using the current timestamp, like so:
```
2020-03-11 19:14:42: Begin [Internal] timetracker doc
```
The 'End' macro copies the category and task name from the previous 'Begin' macro, to cut down on the amount of typing (and eliminate the possibility of typos). It then prints another 'Begin' line, ready for the next task. The output using the line above would look something like this:
```
2020-03-11 19:14:42: Begin [Internal] timetracker doc
2020-03-11 19:30:36: End [Internal] timetracker doc
2020-03-11 19:30:36: Begin
```
I also have Keyboard Maestro text string triggers for common categories, to minimize typing. For instance, the text string triggers `[css` becomes `[Internal] `, `[pro` becomes `[Internal] `, `[mm` or `[MM` becomes `[Mass Mutual] `, `[vzw` becomes `[Verizon Wireless] `, `[pin` becomes `[Company] ` (to match the Pindrop related items in Clarizen), etc. These are included in `timetracker.kmmacros` for your convenience.
See [INSTALL.md](https://github.atl.pdrop.net/tblancher/timetracker/blob/master/INSTALL.md) in this repository for a detailed explanation of how to use the `timetracker.kmmacros` and set up the vim macros.

38
chug.sh Executable file
View File

@ -0,0 +1,38 @@
#!/bin/zsh
PATH=/Users/tblancher/bin:/Users/tblancher/homebrew/opt/coreutils/libexec/gnubin:/Users/tblancher/homebrew/opt/grep/libexec/gnubin:/usr/bin:/bin
IGNORE_DO_PROCESS=1
[[ "${1}" == "-d" ]] && IGNORE_DO_PROCESS=0 && shift
WK_OFFSET=$1
WK_TOTAL=0
[[ -z ${WK_OFFSET} ]] && WK_OFFSET=0
DATE=$(date -d "last Monday - ${WK_OFFSET} weeks")
for date in $(date +%F -d "${DATE}") \
$(date +%F -d "${DATE} + 1 day") \
$(date +%F -d "${DATE} + 2 day") \
$(date +%F -d "${DATE} + 3 day") \
$(date +%F -d "${DATE} + 4 day") \
$(date +%F -d "${DATE} + 5 day") \
$(date +%F -d "${DATE} + 6 day"); do
echo $date
if [[ -f ${date}.log ]]; then
[[ -n $IGNORE_DO_PROCESS ]] || ./do_process.sh ${date}.log | less
daily_total=$(./do_process.sh ${date}.log | grep -P "Grand total:" | grep -Po "\d+\.\d+")
if [[ ${daily_total} -gt 24 ]]; then
daily_total=24.00
fi
else
daily_total=0
fi
echo ${daily_total}
echo
#echo -n Press ENTER for ${date}...
#read
WK_TOTAL=$(bc <<< "scale=2; $WK_TOTAL + $daily_total")
done
echo "Weekly Total: ${WK_TOTAL}"

23
clean_timetracker_staging.zsh Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env zsh
source ~/bin/zendesk_env.sh
get_url="https://${zd_hostname}/api/v2/users/${zd_user_id}/tickets/assigned"
earliest=$(curl --silent --verbose --location --header "Authorization: Basic ${basic_auth_token}" --header "Accept: application/json" --header "Content-Type: application/json" ${get_url} | \
jq -r '.tickets[] | pick(.id, .created_at, .status) | select(.status != "closed") | select(.status != "solved") | .created_at' | sort -u | head -1)
se=$(date -d "${earliest} -1 day" +%s)
print "Earliest open created date is: $(date -d "${earliest}") (${se})" >&2
for log in ~/timetracker/staging/*.log; do
local tstamp=$(sed '/^$/d' ${log} | tail -1 | awk -F: '{print $1":"$2":"$3}')
local st=$(date -d "${tstamp}" +%s)
touch -d ${tstamp} ${log}
if [[ "${st}" -le "${se}" ]]; then
print "Deleting ${log}..." >&2
rm ${log}
else
print "Preserving ${log}..." >&2
fi
done

79
do_process.sh Executable file
View File

@ -0,0 +1,79 @@
#!/Users/tblancher/homebrew/bin/bash
PATH=/Users/tblancher/bin:/Users/tblancher/homebrew/opt/coreutils/libexec/gnubin:/usr/bin:/bin
grep=/Users/tblancher/homebrew/opt/grep/libexec/gnubin/grep
declare -A ORGS
#set -x
if [[ "x${1}" == "x" ]]; then
DATE=$(date +%F).log
else
DATE=$1
fi
if [[ -f ${DATE} ]]; then
find_existing_org () {
for org in ${ORGS[@]}; do
[[ "${org}" == "${1}" ]] && return 0
done
return 1
}
get_list_of_orgs () {
while read; do
org=$( $grep -Po "\[.*\]" <<< "${REPLY}" | tr -d '[][]' )
[[ "${org}x" == "x" ]] && continue
# org="${org##*( )}" # trim leading whitespace
# org="${org%%*( )}" # trim trailing whitespace
# [[ "${ORGS[@]}" =~ "^${org}$" ]] || ORGS+=("${org}")
# find_existing_org "${org}" || ORGS+=("${org}")
ORGS["${org}"]=1
done
}
get_list_of_orgs < ${DATE}
# IFS=$'\n'; KEYS=($(sort -f <<< "${!ORGS[@]}")); unset IFS
KEYS=()
while IFS= read -rd '' key; do
KEYS+=( "${key}" )
done < <(printf '%s\0' "${!ORGS[@]}" | sort -f -z)
# for (( i=0; i < ${#ORGS[@]}; i++ )); do
for org in "${KEYS[@]}"; do
echo "[${org}]"
done
# for org in "${!ORGS[@]}"; do
# echo "[${org}]"
# done
echo "--"
echo ${DATE}
echo
echo
echo
WHOLE=""
subtotal=0
for pattern in "${KEYS[@]}"; do
#echo "pattern=${pattern}" >&2
$grep -- "\[${pattern}\]" ${DATE} | python timetracker.py
subtotal=$(bc <<< "scale=2; ${subtotal} + $($grep -- "\[${pattern}\]" ${DATE} | python timetracker.py | $grep "Section" | $grep -Po '\d+\.\d+hrs' | tr -d '[hrs]')")
printf "Subtotal: %3.2fhrs\n" ${subtotal}
echo
if [[ "${WHOLE}x" == "x" ]]; then
WHOLE="${pattern}"
else
WHOLE="${WHOLE}|${pattern}"
fi
echo
done
#$grep -Ev "${WHOLE}" ${DATE}.log | timetracker
echo
timetracker ${DATE} | $grep "Section" | sed 's/Section/Grand/'
else
echo "${DATE} does not exist" >&2
fi
#set +x

BIN
images/KM_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
images/change_trigger.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

BIN
images/conflict_dialog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
images/enable_macros.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

BIN
images/import_menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

36
month-pack.sh Executable file
View File

@ -0,0 +1,36 @@
#!/bin/bash
PATH=/Users/tblancher/homebrew/opt/coreutils/libexec/gnubin:/Users/tblancher/homebrew/opt/gnu-tar/libexec/gnubin:/Users/tblancher/bin:/Users/tblancher/homebrew/bin:/Users/tblancher/homebrew/sbin:/Users/tblancher/gem/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/sbin:/usr/sbin:/Users/tblancher/bin
if [[ -z "$1" ]]; then
MONTH=$(( $(date +%-m) - 1 ))
else
MONTH=$1
shift
fi
if [[ -z "$1" ]]; then
YEAR=$(date +%Y)
CURR=1
else
YEAR=$1
shift
CURR=0
fi
if [[ "$MONTH" -eq 0 ]];
then
MONTH=12
fi
if [[ "${CURR}" -eq 1 ]] && [[ $(( $(date +%-m) - MONTH )) -le 0 ]]; then
YEAR=$(( YEAR - 1))
fi
if [ "$MONTH" -lt 10 ];
then
MONTH=0$MONTH
fi
tar -cvJf "$YEAR-$MONTH.tar.xz" $YEAR-$MONTH-*.log --remove-files

436
rust/timetracking/Cargo.lock generated Normal file
View File

@ -0,0 +1,436 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "0.7.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e"
dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bumpalo"
version = "3.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
[[package]]
name = "cc"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
dependencies = [
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"time",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "chug"
version = "0.1.0"
dependencies = [
"chrono",
"itertools",
"regex",
"timelogging",
]
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "cxx"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b7d4e43b25d3c994662706a1d4fcfc32aaa6afd287502c111b237093bb23f3a"
dependencies = [
"cc",
"cxxbridge-flags",
"cxxbridge-macro",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84f8829ddc213e2c1368e51a2564c552b65a8cb6a28f31e576270ac81d5e5827"
dependencies = [
"cc",
"codespan-reporting",
"once_cell",
"proc-macro2",
"quote",
"scratch",
"syn",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e72537424b474af1460806647c41d4b6d35d09ef7fe031c5c2fa5766047cc56a"
[[package]]
name = "cxxbridge-macro"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "309e4fb93eed90e1e14bea0da16b209f81813ba9fc7830c20ed151dd7bc0a4d7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "doprocess"
version = "0.1.0"
dependencies = [
"itertools",
"regex",
"timelogging",
]
[[package]]
name = "either"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]]
name = "iana-time-zone"
version = "0.1.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c422fb4f6e80490d0afcacf5c3de2c22ab8e631e0cd7cb2d4a3baf844720a52a"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
dependencies = [
"cxx",
"cxx-build",
]
[[package]]
name = "itertools"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484"
dependencies = [
"either",
]
[[package]]
name = "js-sys"
version = "0.3.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
[[package]]
name = "link-cplusplus"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
dependencies = [
"cc",
]
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]]
name = "proc-macro2"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
[[package]]
name = "scratch"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
[[package]]
name = "syn"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [
"winapi-util",
]
[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi",
"winapi",
]
[[package]]
name = "timelogging"
version = "0.1.0"
dependencies = [
"chrono",
"itertools",
"regex",
]
[[package]]
name = "timetracker"
version = "0.1.1"
dependencies = [
"chrono",
"itertools",
"regex",
"timelogging",
]
[[package]]
name = "unicode-ident"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasm-bindgen"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

View File

@ -0,0 +1,9 @@
[workspace]
resolver = "2"
members = [
"timetracker",
"doprocess",
"chug",
"timelogging"
]

View File

@ -0,0 +1,12 @@
[package]
name = "chug"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
regex = "1"
chrono = "0.4"
itertools = "0.8"
timelogging = { path = "../timelogging" }

View File

@ -0,0 +1,73 @@
use std::env;
use std::fs;
use std::io::{BufReader, BufRead, ErrorKind};
use regex::Regex;
use chrono::{Datelike, Duration, offset::Local, NaiveDate, Weekday};
use timelogging::timetracking::{process_input_file, generate_individual_timecards};
fn main() {
// Regular Expressions
let offset = Regex::new(r"^[+-]*\d+").unwrap();
// Process options
let params: Vec<String> = env::args().collect();
// If week offset is provided, grab it
let mut week_offset: i64 = 0;
for arg in &params {
if offset.is_match(&arg.to_string()) {
week_offset = arg.parse::<i64>().unwrap();
};
};
let day: NaiveDate = last_monday(week_offset);
let mut filenames: Vec<String> = Vec::new();
for d in 0..7 {
filenames.push((day + Duration::days(d)).format("%Y-%m-%d.log").to_string());
};
assert_eq!(filenames.len(), 7);
let mut grand_total: f64 = 0.0;
for filename in filenames {
println!("{} ", filename);
// Process the file
let mut gtoth = match fs::File::open(filename) {
Ok(file) => {
// Hash Maps (akin to Python Dictionaires)
let mut reader: Box<dyn BufRead> = Box::new(BufReader::new(file));
// For each line of input (build internal data structures)
let (mut start, mut finish, _) = process_input_file(&mut reader);
// generate total for this file
let (_, total) = generate_individual_timecards(&mut start, &mut finish);
total
},
Err(error) => match error.kind() {
ErrorKind::NotFound => 0.0,
_ => panic!("Error reading file! Error: {}", error)
}
};
if gtoth > 24.0 {
gtoth = 24.0;
};
// print the output
println!("{}", format!("{:.2}", gtoth));
println!();
grand_total += gtoth;
};
println!("Grand total: {:.2}", grand_total);
}
fn last_monday(offset: i64) -> NaiveDate {
let day: NaiveDate = Local::today().naive_local() - Duration::days(7 * offset);
match day.weekday() {
Weekday::Mon => day - Duration::days(0),
Weekday::Tue => day - Duration::days(1),
Weekday::Wed => day - Duration::days(2),
Weekday::Thu => day - Duration::days(3),
Weekday::Fri => day - Duration::days(4),
Weekday::Sat => day - Duration::days(5),
Weekday::Sun => day - Duration::days(6)
}
}

View File

@ -0,0 +1,12 @@
[package]
name = "doprocess"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
regex = "1"
itertools = "0.8"
timelogging = { path = "../timelogging" }

View File

@ -0,0 +1,58 @@
use std::env;
use std::fs;
use std::io::{self, BufReader, BufRead};
use regex::Regex;
use itertools::Itertools;
use timelogging::timetracking::{nearest, process_input_file, generate_individual_timecards};
fn main() {
// set up a regex for escaping regex characters
let cat_esc = Regex::new(r"(?P<char>[-*^$+?])").unwrap();
// Process the file (stdin or file argument)
let input = env::args().nth(1);
let mut reader: Box<dyn BufRead> = match input {
None => Box::new(BufReader::new(io::stdin())),
Some(filename) => Box::new(BufReader::new(fs::File::open(filename).unwrap()))
};
// For each line of input (build internal data structures)
let (mut start, mut finish, categories) = process_input_file(&mut reader);
// generate individual timecards
let (ind, gtoth) = generate_individual_timecards(&mut start, &mut finish);
// print the output
let mut running_total: f64 = 0.0;
for cat in categories {
let mut subtotal: f64 = 0.0;
let catre = Regex::new(&format!(r"\[{}\]\s+.*\s*$",
cat_esc.replace_all(&cat, r"\$char".to_string()))
.to_string())
.unwrap();
for (act, duration) in ind.iter().sorted_by_key(|x| x.0) {
if ! catre.is_match(&act.to_string()) {
continue
};
let mut f: f64 = nearest(*duration as f64/3600.00);
if f == 0.0 {
f = 0.08;
};
let fhrs: String = format!("{:.2}hrs", f);
println!("{}", format!("{:<75}{:>10}", act.to_string(), fhrs.to_string()));
subtotal += f;
};
running_total += subtotal;
println!();
println!("{}", format!("{:<20}{:.2}hrs", "Section total:", subtotal));
println!("{}", format!("{:<20}{:.2}hrs", "Subtotal:", running_total));
println!();
println!();
};
println!();
println!("{}", format!("Grand total: {:.2}", gtoth));
}

View File

@ -0,0 +1,11 @@
[package]
name = "timelogging"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
regex = "1"
chrono = "0.4"
itertools = "0.8"

View File

@ -0,0 +1,136 @@
pub mod timetracking {
use std::io::BufRead;
use std::collections::HashMap;
use regex::Regex;
use chrono::NaiveDateTime;
use itertools::Itertools;
pub fn nearest(hr: f64) -> f64 {
let t = hr * 4.0;
t.round() / 4.0
}
pub fn process_input_file(reader: &mut Box<dyn BufRead>) -> (
HashMap<String, Vec<i64>>,
HashMap<String, Vec<i64>>,
Vec<String>) {
let mut start: HashMap<String, Vec<i64>> = HashMap::new();
let mut finish: HashMap<String, Vec<i64>> = HashMap::new();
let mut categories: Vec<String> = Vec::new();
// regex
let empty = Regex::new(r"^\s*$").unwrap(); // regex for finding empty lines (and skipping them)
let comment = Regex::new(r"^\s*#").unwrap(); // regex for finding empty lines (and skipping them)
let delim = Regex::new(r":\s{2}").unwrap(); // regex for splitting timestamp entry
let begin = Regex::new(r"Begin\s+(?P<action>.*)\s*$").unwrap();
let end = Regex::new(r"End\s+(?P<action>.*)\s*$").unwrap();
let category = Regex::new(r"\[(?P<category>.*)\]\s+(?P<activity>.*)\s*$").unwrap();
for line in reader.lines() {
// need to get the string out of line (which is an option enum)
let l: String = line.unwrap();
// if the line is empty or is a comment
if empty.is_match(&l) || comment.is_match(&l) {
continue
};
// Parse the timestamp and entry
let splits: Vec<_> = delim.split(&l.trim_end()).into_iter().collect();
// splits should only ever have two elements (only one delimiter per log line)
let datetime = splits[0];
let entry = splits[1];
let date_time = NaiveDateTime::parse_from_str(datetime,"%Y-%m-%d %H:%M:%S").unwrap();
let timestamp = date_time.timestamp();
// Process a Begin
if begin.is_match(&entry) {
let caps = begin.captures(&entry).unwrap();
let action = caps.name("action").unwrap().as_str();
start.entry(action.to_string()).or_insert(Vec::new()).push(timestamp);
let cats = category.captures(&entry).unwrap();
let cat = cats.name("category").unwrap().as_str();
categories.push(cat.to_string());
};
// Process an End
if end.is_match(&entry) {
let caps = end.captures(&entry).unwrap();
let action = caps.name("action").unwrap().as_str();
finish.entry(action.to_string()).or_insert(Vec::new()).push(timestamp);
let cats = category.captures(&entry).unwrap();
let cat = cats.name("category").unwrap().as_str();
categories.push(cat.to_string());
};
}; // end for
// sort and dedup categories
categories.sort_unstable();
categories.dedup();
(start, finish, categories)
}
pub fn generate_individual_timecards (
start: &mut HashMap<String, Vec<i64>>,
finish: &mut HashMap<String, Vec<i64>>
) -> (
HashMap<String, i64>,
f64
)
{
let mut ind: HashMap<String, i64> = HashMap::new();
let mut gtoth: f64 = 0.0;
for (act, tstamps) in start.iter_mut().sorted_by_key(|x| x.0) {
let bc: i64 = tstamps.len().try_into().unwrap();
let mut ec: i64 = 0;
if finish.contains_key(&act.to_string()) {
ec = finish.get(&act.to_string()).expect("Somehow, not a vector!").len().try_into().unwrap();
};
// Debugging
// println!("bc: {}, ec: {}", bc, ec);
// This block ensures the lengths of start and finish for this action are equal
if bc - ec > 1 {
panic!("ERROR: Missing more than one End"); // need to fix log file
} else if bc > ec { // bc should be exactly one greater
tstamps.pop();
} else if ec > bc {
panic!("ERROR: Missing a Begin"); // need to fix log file
};
// Debugging
//for t in tstamps.into_iter() {
// print!("{} ", t);
//};
//println!();
//for t in finish.get_mut(&act.to_string()).expect("Somehow, not a vector!").into_iter() {
// print!("{} ", t);
//};
//println!();
let mut delta: i64 = 0;
for i in 0..tstamps.len() {
let beg = tstamps[i];
let en = finish.get_mut(&act.to_string()).unwrap()[i];
delta += en - beg;
};
ind.insert(act.to_string(), delta);
let d: f64 = nearest(delta as f64 / 3600.00);
if d == 0.0 {
gtoth += 0.08;
} else {
gtoth += d;
};
}
(ind, gtoth)
}
}

View File

@ -0,0 +1,12 @@
[package]
name = "timetracker"
version = "0.1.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
regex = "1"
chrono = "0.4"
itertools = "0.8"
timelogging = { path = "../timelogging" }

View File

@ -0,0 +1,36 @@
use std::env;
use std::fs;
use std::io::{self, BufReader, BufRead};
use itertools::Itertools;
use timelogging::timetracking::{nearest, process_input_file, generate_individual_timecards};
fn main() {
// Process the file (stdin or file argument)
let input = env::args().nth(1);
let mut reader: Box<dyn BufRead> = match input {
None => Box::new(BufReader::new(io::stdin())),
Some(filename) => Box::new(BufReader::new(fs::File::open(filename).unwrap()))
};
// For each line of input (build internal data structures)
let (mut start, mut finish, _) = process_input_file(&mut reader);
// generate individual timecards
let (ind, gtoth) = generate_individual_timecards(&mut start, &mut finish);
// print the output
for (act, duration) in ind.iter().sorted_by_key(|x| x.0) {
let f: f64 = nearest(*duration as f64/3600.00);
let mut fhrs: String = format!("{:2}hrs", 0.08);
if f > 0.0 {
fhrs = format!("{:.2}hrs", f);
};
println!("{}", format!("{:<75}{:>10}", act.to_string(), fhrs.to_string()));
};
println!();
let gtotm: f64 = nearest((gtoth * 60.00) as f64);
println!("{}", format!("Grand total: {:.2}hrs ({:.0} minutes)", gtoth, gtotm));
}

173
timetracker-sections.pl Executable file
View File

@ -0,0 +1,173 @@
#!/usr/bin/perl -w
use strict;
use warnings;
use Date::Manip;
use Math::Round;
my $datetime; # Stores date and time pulled from log
my $entry; # Stores the log entry for processing
my $action; # Stores the action for processing (this will be the key to the
# hash).
my $time; # the $datetime converted to UNIX timestamp
my %begin; # Data structure for storing the beginning of a time frame
my %end; # Data structure for storing the end of a time frame
my %ind; # Data structure for storing individual timecards
my %misc; # Data structure for storing miscellaneous (<0.25hrs) timecards
my ($bc,$ec); # variables for determining if we have a complete set.
while(<>) {
if(/^\s*$/) { # If current line is an empty string, skip to next line
next;
}
chomp;
($datetime,$entry) = split /:\s{1,2}/;
#print STDERR "$datetime: $entry\n";
$time = UnixDate($datetime, "%s");
#print STDERR "$time: $entry\n";
if($entry =~ /^Begin/) {
#print STDERR "DEBUG: Writing an BEGIN action\n";
#print STDERR "$time: ";
$action = $entry;
$action =~ s/^Begin\s+(.+)\s*/$1/;
$action =~ s/\s+$//; # trim off any whitespace at the end
#print STDERR "$action\n";
push(@{$begin{$action}},$time);
}
if($entry =~ /^End/) {
#print STDERR "DEBUG: Writing an END action\n";
#print STDERR "$time: ";
$action = $entry;
$action =~ s/^End\s+(.+)\s*/$1/;
$action =~ s/\s+$//; # trim off any whitespace at the end
#print STDERR "$action\n";
push(@{$end{$action}},$time);
}
}
# We're here, begin processing
my $act;
my $gtot = 0;
foreach $act (sort keys %begin) { # For every activity we started.
# error checking
$bc = 0;
$ec = 0;
$bc = @{$begin{$act}}; # or die "Activity \"$act\" is incomplete. Missing Begin.\n";
#print STDERR "$act\n";
$ec = @{$end{$act}}; # or die "Activity \"$act\" is incomplete. Missing End.\n";
#print STDERR "DEBUG: \"$act\" bc=$bc,ec=$ec\n";
if(($bc - $ec) > 1) {
# haven't logged more than one closing time
# print STDERR "Activity \"$act\" is incomplete. Missing multiple Ends.\n";
exit 2;
} elsif($bc > $ec) { # should be exactly one greater
# print STDERR "Activity \"$act\" is incomplete. Missing one End. Removing last from array.\n";
pop(@{$begin{$act}});
# GRATUITOUS USE OF GOTO HERE!
goto THERE;
# END GRATUITOUS USE OF GOTO HERE!
} elsif($ec > $bc) {
# have missed a begin time
# print STDERR "Activity \"$act\" is incomplete. Missing Begin.\n";
exit 1;
} else {
THERE:
my $sum = 0;
# DOIT
while(@{$begin{$act}} && @{$end{$act}}) {
my $beg = shift @{$begin{$act}};
my $en = shift @{$end{$act}};
$sum += $en - $beg;
}
#printf ("\"$act\": %10ds %3.2fhr\n", $sum, ($sum / 3600 >= 0.25)?nearest(0.25, $sum / 3600):$sum / 3600);
#printf ("\"$act\":\t\t%10ds\t%3.2fhrs\n", $sum, $sum / 3600);
# If sum is greater than fifteen minutes, put it individual list
# otherwise, put it in
# 2014-10-02: Remove miscellaneous, since we will no longer be reporting in
# fifteen minute increments
#$sum > 900?$ind{$act}=$sum:$misc{$act}=$sum;
$ind{$act}=$sum;
$gtot += $sum;
}
}
#my $spr;
my $sec;
my $hrs;
my $min;
my $fhrs;
my $ctot = 0;
my $utot = 0;
# DEBUGGING
#foreach $act (sort keys %ind) {
# print STDERR "Activity: " . $act . " ";
# print STDERR "$ind{$act}s " . $ind{$act}/3600 . "hrs ";
# print STDERR $ind{$act}%3600/60 . "min\n";
#}
format STDOUT =
@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @>>>>>>>>>> @>>>>>>>>>> @>>>>>>>>>> @>>>>>>>>>>
$act, $sec, $hrs, $min, $fhrs
.
foreach $act (sort keys %ind) {
#$spr = sprintf("%3.2fhrs",nearest(0.25, $ind{$act}/3600));
$min = sprintf("%dmin",nearest(1, $ind{$act}%3600/60));
$hrs = sprintf("%dhrs",$ind{$act}/3600);
$fhrs = sprintf("%.2fhrs",$ind{$act}/3600);
$sec = "$ind{$act}s";
write;
if($act =~ /^\d{8}/) {
#print STDERR "Got a case\n";
$ctot += $ind{$act};
} else {
#print STDERR "Got a non-case\n";
$utot += $ind{$act};
}
# } elsif($changed == 0) {
# print "\n";
# printf ("Case total: %10ds, %3.2fhrs, %3.2fhrs (rounded)\n", $ctot, $ctot / 3600, nearest(0.25, $ctot / 3600));
# print "\n";
# $changed = 1;
#}
# else {
# $min = sprintf("%dmin",nearest(1, $ind{$act}%3600/60));
# $hrs = sprintf("%dhrs",$ind{$act}/3600);
# $fhrs = sprintf("%.2fhrs",$ind{$act}/3600);
# $sec = "$ind{$act}s";
# write;
# $utot += $ind{$act};
# }
}
print "\n";
printf ("Case total: %10ds, %3.2fhrs, %3.2fhrs (rounded)\n", $ctot, $ctot / 3600, nearest(0.25, $ctot / 3600));
print "\n";
print "\n";
printf ("Unnumbered total: %10ds, %3.2fhrs, %3.2fhrs (rounded)\n", $utot, $utot / 3600, nearest(0.25, $utot / 3600));
print "\n";
# REMOVED 2014-10-02
#$tot = 0;
#foreach $act (sort keys %misc) {
# $spr = sprintf("%3.2fhrs", $misc{$act}/3600);
# $sec = "$misc{$act}s";
# write;
# $tot += $misc{$act};
#}
#print "\n";
#printf ("Miscellaneous total: %10ds, %3.2fhrs, %3.2fhrs (rounded)\n", $tot,$tot / 3600, nearest(0.25, $tot / 3600));
#print "\n";
$gtot = $ctot + $utot;
print "\n";
printf "Grand Total: %10ds, %.02fhrs\n", $gtot, $gtot / 3600;
#my &section_post {
#
#};

1366
timetracker.kmmacros Normal file

File diff suppressed because it is too large Load Diff

115
timetracker.py Executable file
View File

@ -0,0 +1,115 @@
#!/usr/bin/env python3
import fileinput
import re
import sys
import time
def nearest(hr):
return round(hr*4)/4
# Variables
start = {} # Data structure for storing the beginning of a time frame
finish = {} # Data structure for storing the end of a time frame
ind = {} # Data structure for storing individual timecards
misc = {} # Data structure for storing miscellaneous timecards
empty = re.compile("^\s*$") # regex for finding empty lines (and skipping them)
comment = re.compile("^\s*#") # regex for finding empty lines (and skipping them)
delim = re.compile(":\s{2}") # regex for splitting timestamp entry
begin = re.compile("^Begin\s+(?P<action>.*)\s*$")
end = re.compile("^End\s+(?P<action>.*)\s*$")
#case = re.compile("^(PDROP|CHG)-?\d+")
for line in fileinput.input():
if empty.match(line) or comment.match(line): # if the current line is empty
continue # skip to the next line
(datetime,entry) = delim.split(line.strip())
timestamp = time.mktime(time.strptime(datetime,"%Y-%m-%d %H:%M:%S"))
if begin.match(entry):
action = begin.sub("\g<action>",entry)
if action not in start:
start[action] = []
start[action].append(timestamp)
if end.match(entry):
action = end.sub("\g<action>",entry)
if action not in finish:
finish[action] = []
finish[action].append(timestamp)
# We're here, we've captured all of the data
gtoth = 0
gtot = 0
for act in sorted(start.keys()):
if act in start:
bc = len(start[act])
else: bc = 0
if act in finish:
ec = len(finish[act])
else: ec = 0
if bc - ec > 1:
print("ERROR: Missing more than one End", file=sys.stderr)
sys.exit(2)
elif bc > ec: # bc should be exactly one greater
start[act].pop()
elif ec > bc:
print("ERROR: Missing a Begin", file=sys.stderr)
sys.exit(1)
delta = 0
while (len(start[act]) > 0 and len(finish[act]) > 0):
beg = start[act].pop(0)
en = finish[act].pop(0)
delta += en - beg
ind[act] = delta
gtot += delta
if nearest(delta/3600.00) == 0:
gtoth += 0.08
else:
gtoth += nearest(delta/3600.00)
#ttot = 0
#utot = 0
table = "{a:<50} {f:>10}"
for act in sorted(ind.keys()):
#minutes = "{:d}min".format(int(ind[act]%3600/60))
#hrs = "{:d}hrs".format(int(nearest(ind[act]/3600)))
if nearest(ind[act]/3600.0) == 0:
fhrs = "{:.2f}hrs".format(0.08)
else:
fhrs = "{:.2f}hrs".format(nearest(ind[act]/3600.00))
#sec = "{:d}s".format(int(ind[act]))
#print table.format(a=act, s=sec, h=hrs, m=minutes, f=fhrs)
print(table.format(a=act, f=fhrs))
# if case.match(act):
# ttot += ind[act]
# else:
# utot += ind[act]
total = "{t:<20} {h:>10}"
#ttots = "{:d}s".format(int(ttot))
#utots = "{:d}s".format(int(utot))
#ttoth = "{:3.2f}hrs".format(nearest(ttot / 3600))
#utoth = "{:3.2f}hrs".format(nearest(utot / 3600))
#gtots = "{:d}s".format(int(gtot))
gtoth = "{:3.2f}hrs".format(gtoth)
print()
#print total.format(t="Incident total:", s=ttots, h=ttoth)
#print total.format(t="Unnumbered total:", s=utots, h=utoth)
print(total.format(t="Section total:", h=gtoth))

1
timetracker.vim Normal file
View File

@ -0,0 +1 @@
autocmd BufRead,BufNewFile *.log set filetype=timetracker

2
timetracker_macros.vim Normal file
View File

@ -0,0 +1,2 @@
klYjp
kly$jp

4
vim/syntax/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*
!README.md
!timetracker.vim
!.gitignore

9
vim/syntax/README.md Normal file
View File

@ -0,0 +1,9 @@
# timetracker.vim syntax file and Git
The _timetracker.vim_ syntax file I use at my employer contains customer names
so it's easy to tell the parts of my timetracker log lines are for particular
customers (or other important entities). Since this information is sensitive,
I am sanitizing it before I post it to my personal, public-facing Git repo on
git.eldon.me.
Just run `./toggle_remote.sh` and it should do the right thing.

100
vim/syntax/timetracker.vim Normal file
View File

@ -0,0 +1,100 @@
" Vim syntaxtax file
" Language: timetracker log
" Creator: Trey Blancher $(base64 -d <<< dGJsYW5jaGVyQHBpbmRyb3AuY29tCg==)
" Latest revision: 2024-06-25
if exists("b:current_syntax")
finish
endif
syntax keyword timetrackerCategories Cloud Call Delivery Internal On Prem PTO transparent contained
syntax keyword timetrackerStartStop Begin End
syntax keyword timetrackerStandardOps
\ API
\ APT
\ AST
\ AVT
\ All
\ Auth
\ Authenticate
\ Behavior
\ Bio
\ Call
\ Certified Kubernetes Administrator
\ Clarizen
\ Correlation
\ Device
\ DRE
\ DSM
\ Express
\ Feedback
\ Grafana
\ Hands
\ Identity
\ IR
\ Keyboard
\ Maestro
\ Management
\ PCPN
\ Passport
\ PIN token renewal
\ PMR
\ Prometheus
\ Protect
\ Push
\ Resource
\ Risk
\ Slack
\ Transfer
\ VeriCall
\ Voice
\ Vormetric
\ access
\ behaviorprint
\ daily
\ email
\ handling
\ lesson
\ macOS
\ meeting
\ phoneprint
\ prep
\ scheduled
\ standup
\ timesheets
\ timetracker
\ triage
\ upgrades
\ verification
\ voiceprint
syntax keyword Customers
\ Customer 1
\ Customer 2
\ Customer 3
syntax match timetrackerTimestamp /\d\{4}-\%(0[135789]-\%([0-2]\d\|3[01]\)\|\%(1[02]-\%([0-2]\d\|3[01]\)\)\|0[46]-\%([0-2]\d\|30\)\|11-\%([0-2]\d\|30\)\|02-[0-2]\d\) \%([01]\d\|2[0-3]\):\%([0-5]\d\)\%(:[0-5]\d\)\{2}/ contained
"syntax match ticket /\(#|CM-|PD-\)\d\+/
"syntax match timetrackerTimestampError /^\(\d\{4}-\d\{2}-\d\{2} \d\{2}:\d\{2}:\d\{2}\)\@!/ contained
syntax match timetrackerTask /.*/ contained
"syntax match timetrackerTimestampError /^\(\(\d\{4}-\d\{2}-\d\{2} \d\{2}:\d\{2}:\d\{2}\)\@!\)/ transparent contained
"syntax region errTimestamp matchgroup=timestamp start=/^\(\d\{4}-\(0\d\|1[0-2]\)-\d\{2} \d\{2}:\d\{2}:\d\{2}\)\@!/ end=/: / contains=timetrackerTimestamp
syntax region timestampGroup start=/^/ end=/: / contains=timetrackerTimestamp
syntax region errTimestamp matchgroup=timestamp start="^\%(\d\{4}-\%(\%(0[13578]\|1[02]\)-\%([0-2]\d\|3[01]\)\|\%(0[469]\|11\)-\%([0-2]\d\|30\)\|02-[0-2]\d\) \%([01]\d\|2[0-3]\)\%(:[0-5]\d\)\{2}\)\@!" end=": "
syntax region category start=/\[/ end=/\]/ contains=timetrackerCategories
syntax region ticket start=/\(#\|CM-\|PD-\|NETENG-\|OPS-\|INC-\)\d\{-1}/ end=/\d /
"syntax region item matchgroup=task start=/\]\s[\k ]\+/ end=/$/ contains=ticket
let b:current_syntaxtax = "timetracker"
highlight default link errTimestamp Error
highlight default link timestamp String
highlight default link timestampGroup String
highlight default link timetrackerStartStop Statement
highlight default link category Type
highlight default link ticket Constant
highlight default link Customers Identifier
highlight default link timetrackerStandardOps Define

24
year-pack.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
PATH=/Users/tblancher/homebrew/opt/coreutils/libexec/gnubin:/Users/tblancher/homebrew/opt/gnu-tar/libexec/gnubin:/Users/tblancher/bin:/Users/tblancher/homebrew/bin:/Users/tblancher/homebrew/sbin:/Users/tblancher/gem/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/sbin:/usr/sbin:/Users/tblancher/bin
MONTH=$(( $(date +%m) - 1 ))
if [[ -z "$1" ]]
then
YEAR=$(( $(date +%Y) - 1))
ONESET=true
else
YEAR=$1
fi
echo "Tarring up for year ${YEAR}..."
if [ $MONTH -eq 0 ];
then
MONTH=12
if [[ ! ${ONESET} ]]; then
YEAR=$(( YEAR - 1))
fi
fi
tar -cvJf "$YEAR.tar" $YEAR-*.tar.xz --remove-files