diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ca65714
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/out/
+/*.jar
\ No newline at end of file
diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml
new file mode 100644
index 0000000..c96f5d3
--- /dev/null
+++ b/resources/META-INF/plugin.xml
@@ -0,0 +1,46 @@
+
+ com.darkyen.darkyenustimetracker
+ Darkyenus Time Tracker
+ 1.0
+ Darkyen
+ Miscellaneous
+
+
+ Adds a single status bar widget: click to start counting, click again to stop.
+ Pauses the timer automatically when idle (after two minutes of inactivity).
+ Time is saved in IDE's workspace files, does not clutter project's directory.
+
+ Source available on GitHub
+ ]]>
+
+ Version 1.0
+
+ ]]>
+
+
+
+
+
+
+ com.intellij.modules.platform
+
+
+
+
+
+
+
+
+
+
+
+ com.darkyen.TimeTrackerComponent
+
+
+
+
\ No newline at end of file
diff --git a/src/com/darkyen/TimeTrackerComponent.java b/src/com/darkyen/TimeTrackerComponent.java
new file mode 100644
index 0000000..3304d81
--- /dev/null
+++ b/src/com/darkyen/TimeTrackerComponent.java
@@ -0,0 +1,81 @@
+package com.darkyen;
+
+import com.intellij.openapi.components.*;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.wm.WindowManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ *
+ */
+@State(
+ name="DarkyenusTimeTracker",
+ storages = {@Storage(value = StoragePathMacros.WORKSPACE_FILE, roamingType = RoamingType.DEFAULT)}
+ )
+public final class TimeTrackerComponent implements ProjectComponent, PersistentStateComponent {
+
+ private final Project project;
+ private TimeTrackerWidget widget;
+
+ private TimeTrackerState lastStateCache = null;
+
+ public TimeTrackerComponent(Project project) {
+ this.project = project;
+ }
+
+ @Override
+ public void initComponent() {
+ }
+
+ @Override
+ public void disposeComponent() {
+ }
+
+ @Override
+ @NotNull
+ public String getComponentName() {
+ return "TimeTrackerComponent";
+ }
+
+ @Override
+ public void projectOpened() {
+ if (widget == null) {
+ widget = new TimeTrackerWidget();
+ if (lastStateCache != null) {
+ widget.setState(lastStateCache);
+ lastStateCache = null;
+ }
+ WindowManager.getInstance().getStatusBar(project).addWidget(widget);
+ }
+ }
+
+ @Override
+ public void projectClosed() {
+ if (widget != null) {
+ WindowManager.getInstance().getStatusBar(project).removeWidget(widget.ID());
+ lastStateCache = widget.getState();
+ }
+ }
+
+ @Nullable
+ @Override
+ public TimeTrackerState getState() {
+ if (widget != null) {
+ return widget.getState();
+ } else if(lastStateCache != null) {
+ return lastStateCache;
+ } else {
+ return new TimeTrackerState();
+ }
+ }
+
+ @Override
+ public void loadState(TimeTrackerState state) {
+ if (widget != null) {
+ widget.setState(state);
+ } else {
+ lastStateCache = state;
+ }
+ }
+}
diff --git a/src/com/darkyen/TimeTrackerState.java b/src/com/darkyen/TimeTrackerState.java
new file mode 100644
index 0000000..57fc74c
--- /dev/null
+++ b/src/com/darkyen/TimeTrackerState.java
@@ -0,0 +1,10 @@
+package com.darkyen;
+
+/**
+ *
+ */
+@SuppressWarnings("WeakerAccess")
+public final class TimeTrackerState {
+ public long totalTimeSeconds = 0;
+ public long idleThresholdMs = 2 * 60 * 1000;
+}
diff --git a/src/com/darkyen/TimeTrackerWidget.java b/src/com/darkyen/TimeTrackerWidget.java
new file mode 100644
index 0000000..8d6412b
--- /dev/null
+++ b/src/com/darkyen/TimeTrackerWidget.java
@@ -0,0 +1,237 @@
+package com.darkyen;
+
+import com.intellij.ide.ui.UISettings;
+import com.intellij.openapi.wm.CustomStatusBarWidget;
+import com.intellij.openapi.wm.StatusBar;
+import com.intellij.openapi.wm.StatusBarWidget;
+import com.intellij.ui.JBColor;
+import com.intellij.util.concurrency.EdtExecutorService;
+import com.intellij.util.ui.JBUI;
+import com.intellij.util.ui.UIUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.AWTEventListener;
+import java.time.Duration;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ *
+ */
+public final class TimeTrackerWidget extends JButton implements CustomStatusBarWidget, AWTEventListener {
+
+ private TimeTrackerState state = new TimeTrackerState();
+
+ private boolean running = false;
+ private long startedAtMs = 0;
+
+ private boolean idle = false;
+ private long lastActivityAtMs = System.currentTimeMillis();
+
+ private ScheduledFuture> ticker;
+
+ TimeTrackerWidget() {
+ addActionListener(e -> setRunning(!running));
+ setBorder(StatusBarWidget.WidgetBorder.INSTANCE);
+ setOpaque(false);
+ setFocusable(false);
+ }
+
+ void setState(TimeTrackerState state) {
+ this.state = state;
+ }
+
+ synchronized TimeTrackerState getState() {
+ final long runningForSeconds = runningForSeconds();
+ if (runningForSeconds > 0) {
+ state.totalTimeSeconds += runningForSeconds;
+ startedAtMs += runningForSeconds * 1000;
+ }
+ return state;
+ }
+
+ private long runningForSeconds() {
+ if (!running) {
+ return 0;
+ } else {
+ return Math.max(System.currentTimeMillis() - startedAtMs, 0) / 1000;
+ }
+ }
+
+ private synchronized void setRunning(boolean running) {
+ if (!this.running && running) {
+ this.running = true;
+ this.startedAtMs = System.currentTimeMillis();
+
+ if (ticker != null) {
+ ticker.cancel(false);
+ }
+ ticker = EdtExecutorService.getScheduledExecutorInstance().scheduleWithFixedDelay(() -> UIUtil.invokeLaterIfNeeded(() -> {
+ final long now = System.currentTimeMillis();
+ if (now - lastActivityAtMs > state.idleThresholdMs) {
+ if (this.running) {
+ setRunning(false);
+ idle = true;
+ }
+ }
+ repaint();
+ }), 1, 1, TimeUnit.SECONDS);
+ } else if(this.running && !running) {
+ state.totalTimeSeconds += runningForSeconds();
+ this.running = false;
+
+ if (ticker != null) {
+ ticker.cancel(false);
+ ticker = null;
+ }
+ }
+ }
+
+ @NotNull
+ @Override
+ public String ID() {
+ return "com.darkyen.DarkyenusTimeTracker";
+ }
+
+ @Nullable
+ @Override
+ public WidgetPresentation getPresentation(@NotNull PlatformType type) {
+ return null;
+ }
+
+ @Override
+ public void install(@NotNull StatusBar statusBar) {
+ Toolkit.getDefaultToolkit().addAWTEventListener(this,
+ AWTEvent.KEY_EVENT_MASK |
+ AWTEvent.MOUSE_EVENT_MASK |
+ AWTEvent.MOUSE_MOTION_EVENT_MASK
+ );
+ }
+
+ @Override
+ public void dispose() {
+ Toolkit.getDefaultToolkit().removeAWTEventListener(this);
+ setRunning(false);
+ }
+
+ private static final Color COLOR_OFF = new JBColor(new Color(189, 0, 16), new Color(128, 0, 0));
+ private static final Color COLOR_ON = new JBColor(new Color(28, 152, 19), new Color(56, 113, 41));
+ private static final Color COLOR_IDLE = new JBColor(new Color(200, 164, 23), new Color(163, 112, 17));
+
+ @Override
+ public void paintComponent(final Graphics g) {
+ long result;
+ synchronized (this) {
+ result = state.totalTimeSeconds + runningForSeconds();
+ }
+ final String info = formatDuration(result);
+
+ final Dimension size = getSize();
+ final Insets insets = getInsets();
+
+ final int totalBarLength = size.width - insets.left - insets.right;
+ final int barHeight = Math.max(size.height, getFont().getSize() + 2);
+ final int yOffset = (size.height - barHeight) / 2;
+ final int xOffset = insets.left;
+
+ g.setColor(running ? COLOR_ON : (idle ? COLOR_IDLE : COLOR_OFF));
+ g.fillRect(insets.left, insets.bottom, totalBarLength, size.height - insets.bottom - insets.top);
+
+ final Color fg = getModel().isPressed() ? UIUtil.getLabelDisabledForeground() : JBColor.foreground();
+ g.setColor(fg);
+ UISettings.setupAntialiasing(g);
+ g.setFont(getWidgetFont());
+ final FontMetrics fontMetrics = g.getFontMetrics();
+ final int infoWidth = fontMetrics.charsWidth(info.toCharArray(), 0, info.length());
+ final int infoHeight = fontMetrics.getAscent();
+ g.drawString(info, xOffset + (totalBarLength - infoWidth) / 2, yOffset + infoHeight + (barHeight - infoHeight) / 2 - 1);
+ }
+
+ private static String formatDuration(long secondDuration) {
+ final Duration duration = Duration.ofSeconds(secondDuration);
+ final StringBuilder sb = new StringBuilder();
+
+ boolean found = false;
+ final long days = duration.toDays();
+ if(days != 0) {
+ found = true;
+ sb.append(days).append(" day");
+ if (days != 1) {
+ sb.append("s");
+ }
+ }
+ final long hours = duration.toHours() % 24;
+ if(found || hours != 0) {
+ if(found) {
+ sb.append(" ");
+ }
+ found = true;
+ sb.append(hours).append(" hour");
+ if (hours != 1) {
+ sb.append("s");
+ }
+ }
+ final long minutes = duration.toMinutes() % 60;
+ if(found || minutes != 0) {
+ if(found) {
+ sb.append(" ");
+ }
+ found = true;
+ sb.append(minutes).append(" min");/*
+ if (minutes != 1) {
+ sb.append("s");
+ }*/
+ }
+ final long seconds = duration.getSeconds() % 60;
+ {
+ if(found) {
+ sb.append(" ");
+ }
+ sb.append(seconds).append(" sec");/*
+ if (seconds != 1) {
+ sb.append("s");
+ }*/
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public JComponent getComponent() {
+ return this;
+ }
+
+ private static Font getWidgetFont() {
+ return JBUI.Fonts.label(11);
+ }
+
+ private static final String SAMPLE_STRING = formatDuration(999999999999L);
+ @Override
+ public Dimension getPreferredSize() {
+ final Insets insets = getInsets();
+ int width = getFontMetrics(getWidgetFont()).stringWidth(SAMPLE_STRING) + insets.left + insets.right + JBUI.scale(2);
+ int height = getFontMetrics(getWidgetFont()).getHeight() + insets.top + insets.bottom + JBUI.scale(2);
+ return new Dimension(width, height);
+ }
+
+ @Override
+ public Dimension getMinimumSize() {
+ return getPreferredSize();
+ }
+
+ @Override
+ public Dimension getMaximumSize() {
+ return getPreferredSize();
+ }
+
+ @Override
+ public void eventDispatched(AWTEvent event) {
+ lastActivityAtMs = System.currentTimeMillis();
+ if (idle) {
+ idle = false;
+ setRunning(true);
+ }
+ }
+}