diff --git a/README.md b/README.md index bbaa836..69f4c4f 100644 --- a/README.md +++ b/README.md @@ -6,26 +6,24 @@ Command line client for [Kimai2](https://www.kimai.org/), the open source, self- To use this program you have to install Kimai2 first! -This client is still under development. See planned features in the next section - -## Current and planned features +## Features This client is not intended to replace the Kimai webUI, so only basic functions, starting and stopping measurements Commands: -- [x] Start, restart and stop measurements -- [x] List active and recent measurements -- [x] List projects and activities +- Start, restart and stop measurements +- List active and recent measurements +- List projects and activities UI: -- [x] Interactive terminal UI with autocomplete -- [x] Classic terminal UI for integration +- Interactive terminal UI with autocomplete +- Classic terminal UI for integration Integration: -- [x] Portable executable for all three platforms -- [x] Installer for windows -- [ ] Generate output for Rainmeter (Windows) (Just like [kimai-cmd](https://github.com/infeeeee/kimai-cmd)) -- [x] Generate output for Argos/Kargos/Bitbar (Gnome, Kde, Mac). More info here: [kimai2-cmd-argos](https://github.com/infeeeee/kimai2-cmd-argos) +- Portable executable for all three platforms +- Installer for Windows +- Generate output for Rainmeter (Windows). More info here: [kimai2-cmd-rainmeter](https://github.com/infeeeee/kimai2-cmd-rainmeter) +- Generate output for Argos/Kargos/Bitbar (Gnome, Kde, Mac). More info here: [kimai2-cmd-argos](https://github.com/infeeeee/kimai2-cmd-argos) Requests for integrations with other softwares are welcomed! Just open an issue and show an example output, what you need. @@ -33,7 +31,7 @@ Requests for integrations with other softwares are welcomed! Just open an issue Download from [releases](https://github.com/infeeeee/kimai2-cmd/releases/latest). -You have to create an API password for your username on your Kimai installation. In Kimai: User menu (Top right corner) -> User profile -> API. +You have to create an API password for your username on your Kimai installation. In Kimai: User menu (Top right corner) -> Edit -> API. ### Notes on Windows @@ -105,6 +103,7 @@ Commands: start [project] [activity] start selected project and activity restart [id] restart selected measurement stop [id] stop all or selected measurement measurements, [id] is optional + rainmeter update rainmeter skin list-active list active measurements list-recent list recent measurements list-projects list all projects @@ -180,32 +179,19 @@ For interactive mode just simply: ``` npm start ``` -or -``` -node kimai2-cmd.js -``` - -For usage with options you have pass a `--` before the options if you start with `npm start`. You don't need this if you don't use options just commands -So this two lines are equivalent, both shows the current version of kimai2-cmd: +For usage with options you have pass a `--` before the options. You don't need this if you don't use options just commands: ``` npm start -- -V -node kimai2-cmd.js -V -``` - -This two are equivalent as well, both starts the project `foo` with the activity `bar` - -``` npm start start foo bar -node kimai2-cmd.js start foo bar ``` On the first run it will ask for your settings, but you can just copy settings.ini.example to settings.ini and modify it with your favorite text editor ## Troubleshooting -If you find a bug open an issue! The client is not finished yet, however all implemented features should work! +If you find a bug open an issue here! ## License diff --git a/kimai2-cmd.js b/kimai2-cmd.js index efba725..f81b1da 100644 --- a/kimai2-cmd.js +++ b/kimai2-cmd.js @@ -9,6 +9,7 @@ const fs = require('fs'); const platform = process.platform const appdata = process.env.appdata +const userprofile = process.env.userprofile //request const request = require('request'); @@ -445,22 +446,27 @@ function printList(settings, arr, endpoint) { * * @param {moment} begin beginning moment * @param {moment} end optional, end moment + * @param {boolean} returnArray optional, returns array if true, returns formatted text if false */ -function formattedDuration(begin, end) { +function formattedDuration(begin, end, returnArray = false) { let momentDuration = moment.duration(moment(end).diff(moment(begin))) - let hrs = momentDuration.hours() - let mins = momentDuration.minutes() + let hrs = momentDuration.hours().toString() + let mins = momentDuration.minutes().toString() - if (hrs.toString().length == 1) { + if (hrs.length == 1) { hrs = "0" + hrs } - if (mins.toString().length == 1) { + if (mins.length == 1) { mins = "0" + mins } - return hrs + ':' + mins + if (returnArray) { + return [hrs, mins] + } else { + return hrs + ':' + mins + } } @@ -616,6 +622,8 @@ function uiAskForSettings() { .then(answers => { let settings = {} settings.serversettings = answers + + //argos/bitbar settings settings.argos_bitbar = {} if (platform == "darwin") { @@ -625,6 +633,16 @@ function uiAskForSettings() { } settings.argos_bitbar.buttonlength = 10 + // rainmeter settings + settings.rainmeter = {} + + if (userprofile) { + settings.rainmeter.skindir = path.join(userprofile, "Documents\\Rainmeter\\Skins\\kimai2-cmd-rainmeter\\kimai2") + } else { + settings.rainmeter.skindir = "" + } + settings.rainmeter.meterstyle = "styleProjects" + const thePath = iniFullPath() if (program.verbose) { console.log('Trying to save settings to: ' + thePath) } @@ -671,6 +689,86 @@ function sanitizeServerUrl(kimaiurl) { return kimaiurl.replace(/\/+$/, ""); } +/** + * Replace all occurenies of chars in string + * + * @param {string} search regex to search for + * @param {string} replacement replacement string + * + */ +String.prototype.replaceAll = function (search, replacement) { + var target = this; + return target.replace(new RegExp(search, 'g'), replacement); +}; + +/* -------------------------------- Rainmeter ------------------------------- */ + +const rainmeterVars = {} +rainmeterVars.Variables = {} +const rainmeterRaw = {} +const rainmeterData = {} + +/** + * Updates rainmeter files + * + * @return {object} settings: all settings read from the settings file + */ +function updateRainmeter(settings) { + kimaiList(settings, 'timesheets/recent', false) + .then(res => { + rainmeterRaw.recent = res[1] + return kimaiList(settings, 'timesheets/active', false) + }) + .then(res => { + // active measurement. Rainmeter only supports one active measurement. + rainmeterVars.Variables.serverUrl = settings.serversettings.kimaiurl + rainmeterVars.Variables.activeRecording = (res[1].length) ? res[1][0].project.name + ' | ' + res[1][0].activity.name : "No active recording" + rainmeterVars.Variables.activeHrs = (res[1].length) ? formattedDuration(res[1][0].begin, undefined, true)[0] : "" + rainmeterVars.Variables.activeMins = (res[1].length) ? formattedDuration(res[1][0].begin, undefined, true)[1] : "" + + //Add first id as default + rainmeterVars.Variables.measurementid = rainmeterRaw.recent[0].id + + if (res[1].length) { + rainmeterVars.Variables.startHidden = 1 + rainmeterVars.Variables.stopHidden = 0 + } else { + rainmeterVars.Variables.startHidden = 0 + rainmeterVars.Variables.stopHidden = 1 + } + + //recent measurements + for (let i = 0; i < rainmeterRaw.recent.length; i++) { + let currMeter = {} + currMeter.Meter = 'String' + currMeter.MeterStyle = settings.rainmeter.meterstyle + currMeter.DynamicVariables = '1' + currMeter.Hidden = "#MenuVis#" + currMeter.Text = rainmeterRaw.recent[i].project.name + ' - ' + rainmeterRaw.recent[i].activity.name + currMeter.leftmouseupaction = ini.unsafe('[!SetVariable measurementid "' + rainmeterRaw.recent[i].id + '"][!UpdateMeasure MeasureStart][!CommandMeasure MeasureStart "Run"]') + + rainmeterData["MeterRecent" + i] = currMeter + } + + let rainmeterVarPath = path.join(settings.rainmeter.skindir, 'kimaiVars.inc') + let rainmeterDataPath = path.join(settings.rainmeter.skindir, 'kimaiData.inc') + + // stringify wraps spec character, rainmeter doesn't like that + let rainmeterDataIni = ini.stringify(rainmeterData).replaceAll('\\\\#', '#').replaceAll('"\\[', '[').replaceAll('\]"', ']').replaceAll('\\\\"', '"') + + // write rainmeter files + fs.writeFileSync(rainmeterVarPath, ini.stringify(rainmeterVars), { encoding: 'utf16le' }) + fs.writeFileSync(rainmeterDataPath, rainmeterDataIni, { encoding: 'utf16le' }) + if (program.verbose) { + console.log("Rainmeter files:") + console.log(rainmeterVarPath, rainmeterDataPath) + console.log("rainmeter data:") + console.log(rainmeterVars) + console.log(rainmeterDataIni) + } + }) +} + /* -------------------------------------------------------------------------- */ /* Settings.ini locations */ /* -------------------------------------------------------------------------- */ @@ -694,7 +792,6 @@ program .description(pjson.description + '. For interactive mode start without any commands. To generate settings file start in interactive mode!') .option('-v, --verbose', 'verbose, longer logging', false) .option('-i, --id', 'show id of elements when listing', false) - // .option('-r, --rainmeter', 'generate rainmeter files') .option('-b, --argosbutton', 'argos/bitbar button output') .option('-a, --argos', 'argos/bitbar output') @@ -734,6 +831,15 @@ program.command('stop [id]') }) }) +program.command('rainmeter') + .description('update rainmeter skin') + .action(function () { + checkSettings() + .then(settings => { + updateRainmeter(settings) + }) + }) + program.command('list-active') .description('list active measurements') .action(function () { diff --git a/kimai2-innosetup.iss b/kimai2-innosetup.iss index 452e0c7..132fcb9 100644 --- a/kimai2-innosetup.iss +++ b/kimai2-innosetup.iss @@ -7,7 +7,7 @@ ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) AppId={{A10BF7B2-6641-4B06-9C68-268B649FCE57} AppName=kimai2-cmd -AppVersion=0.2.4 +AppVersion=1.0.0 AppPublisher=infeeeee AppPublisherURL=https://github.com/infeeeee/kimai2-cmd AppSupportURL=https://github.com/infeeeee/kimai2-cmd @@ -45,6 +45,8 @@ Filename: "{app}\kimai2-cmd.exe"; Description: "{cm:LaunchProgram,kimai2-cmd}"; Filename: "{userappdata}\kimai2-cmd\settings.ini"; Section: "serversettings"; Key: "kimaiurl"; String: "{code:GetKimaiUrl}"; Tasks: createini Filename: "{userappdata}\kimai2-cmd\settings.ini"; Section: "serversettings"; Key: "username"; String: "{code:GetUserName}"; Tasks: createini Filename: "{userappdata}\kimai2-cmd\settings.ini"; Section: "serversettings"; Key: "password"; String: "{code:GetPassword}"; Tasks: createini +Filename: "{userappdata}\kimai2-cmd\settings.ini"; Section: "rainmeter"; Key: "skindir"; String: "{code:GetRainmeterPath}"; Tasks: createini +Filename: "{userappdata}\kimai2-cmd\settings.ini"; Section: "rainmeter"; Key: "meterstyle"; String: "styleProjects"; Tasks: createini [Registry] Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \ @@ -62,6 +64,9 @@ AuthPage := CreateInputQueryPage(wpSelectTasks, AuthPage.Add('Kimai2 url:', False); AuthPage.Add('Username:', False); AuthPage.Add('API password:', False); + AuthPage.Add('Skin folder', False); + AuthPage.Values[3] := ExpandConstant('{userdocs}') + '\Rainmeter\Skins\kimai2-cmd-rainmeter\kimai2'; + end; function ShouldSkipPage(PageID: Integer): Boolean; @@ -94,6 +99,11 @@ begin result := AuthPage.Values[2]; end; +function GetRainmeterPath(Param: String): string; +begin +result := AuthPage.Values[3]; +end; + function NeedsAddPath(Param: string): boolean; var OrigPath: string; diff --git a/package.json b/package.json index fedf650..38b1c8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kimai2-cmd", - "version": "0.2.5", + "version": "1.0.0", "description": "Command line client for Kimai2", "main": "kimai2-cmd.js", "bin": { @@ -9,7 +9,8 @@ "scripts": { "start": "node kimai2-cmd.js", "build-nix": "pkg --out-path builds package.json", - "build-current": "pkg --targets node10 --out-path builds kimai2-cmd.js" + "build-current": "pkg --targets node10 --out-path builds kimai2-cmd.js", + "copy-exe-to-rainmeter": "copy .\\builds\\kimai2-cmd.exe %userprofile%\\Documents\\Rainmeter\\Skins\\kimai2-cmd-rainmeter\\@Resources\\kimai2-cmd\\" }, "keywords": [ "kimai2", diff --git a/settings.ini.example b/settings.ini.example index 9358b5e..5485ea0 100644 --- a/settings.ini.example +++ b/settings.ini.example @@ -5,4 +5,8 @@ password=api_kitten [argos_bitbar] kimaipath=/path/to/kimai2-cmd-macos -buttonlength=10 \ No newline at end of file +buttonlength=10 + +[rainmeter] +skindir=C:\Users\username\Documents\Rainmeter\Skins\kimai2-cmd-rainmeter\kimai2 +meterstyle=styleProjects \ No newline at end of file