Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

# CodeClocker Changelog

## [Unreleased]
## [1.0.9] - 2025-11-16

- Add status bar widget with daily coding activity

## [1.0.8] - 2025-10-29

Expand Down
Binary file modified docs/img/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ public static String getActiveApiKey() {
return null;
}

boolean activitiDataStoppedBeingCollected =
boolean activityDataStoppedBeingCollected =
PropertiesComponent.getInstance()
.getBoolean(ACTIVITY_DATA_STOPPED_BEING_COLLECTED_PROPERTY, false);
if (activitiDataStoppedBeingCollected) {
if (activityDataStoppedBeingCollected) {
LOG.warn("Activity data stopped being collected");
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

public class EnterApiKeyAction extends AnAction {

private static final String ENTER_API_KEY = "Paste your API Key to connect to CodeClocker Hub.";

@Override
public void actionPerformed(@NotNull AnActionEvent e) {
showAction();
Expand All @@ -37,14 +39,14 @@ public static void showAction() {
private static String getText() {
String apiKey = ApiKeyPersistence.getApiKey();
if (isBlank(apiKey)) {
return """
Enter your CodeClocker API Key to start tracking your coding activity.""";
return ENTER_API_KEY;
}

return """
Your current API key: %s
return ("""
Current key: %s

Enter your CodeClocker API Key to start tracking your coding activity."""
"""
+ ENTER_API_KEY)
.formatted(apiKey);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,23 @@ public DailyTimeResponse fetchDailyTimePerProject(String apiKey, ZoneId timeZone
}
}

private record DailyTimePerProjectDto(Map<String, Long> projects) {}
private record DailyTimePerProjectDto(Map<String, ProjectStats> projects) {}

public record ProjectStats(long timeSpentSeconds, long additions, long removals) {}

public static class DailyTimeResponse {
private final Map<String, Long> projects;
private final Map<String, ProjectStats> projects;
private final boolean subscriptionExpired;
private final boolean error;

private DailyTimeResponse(
Map<String, Long> projects, boolean subscriptionExpired, boolean error) {
Map<String, ProjectStats> projects, boolean subscriptionExpired, boolean error) {
this.projects = projects;
this.subscriptionExpired = subscriptionExpired;
this.error = error;
}

public static DailyTimeResponse success(Map<String, Long> projects) {
public static DailyTimeResponse success(Map<String, ProjectStats> projects) {
return new DailyTimeResponse(projects, false, false);
}

Expand All @@ -89,7 +91,7 @@ public static DailyTimeResponse error() {
return new DailyTimeResponse(Collections.emptyMap(), false, true);
}

public Map<String, Long> getProjects() {
public Map<String, ProjectStats> getProjects() {
return projects;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ChangesActivityTracker {

public static final AtomicLong GLOBAL_ADDITIONS = new AtomicLong(0);
public static final AtomicLong GLOBAL_REMOVALS = new AtomicLong(0);

private final Map<String, Map<String, ChangesSample>> fileNameByChangesSample =
new ConcurrentHashMap<>();
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Expand All @@ -18,6 +22,8 @@ public void incrementAdditions(
Lock lock = readWriteLock.readLock();
try {
lock.lock();
GLOBAL_ADDITIONS.addAndGet(additions);

fileNameByChangesSample
.computeIfAbsent(project, p -> new ConcurrentHashMap<>())
.computeIfAbsent(filePath, f -> ChangesSample.create(extension))
Expand All @@ -31,6 +37,8 @@ public void incrementRemovals(String project, String fileName, String extension,
Lock lock = readWriteLock.readLock();
try {
lock.lock();
GLOBAL_REMOVALS.addAndGet(removals);

fileNameByChangesSample
.computeIfAbsent(project, p -> new ConcurrentHashMap<>())
.computeIfAbsent(fileName, f -> ChangesSample.create(extension))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.codeclocker.plugin.intellij.services;

import static com.codeclocker.plugin.intellij.services.ChangesActivityTracker.GLOBAL_ADDITIONS;
import static com.codeclocker.plugin.intellij.services.ChangesActivityTracker.GLOBAL_REMOVALS;
import static com.codeclocker.plugin.intellij.services.TimeSpentPerProjectLogger.GLOBAL_STOP_WATCH;

import com.codeclocker.plugin.intellij.stopwatch.SafeStopWatch;
Expand All @@ -25,8 +27,9 @@ public class TimeTrackerWidgetService implements Disposable {
private final TimeTrackerWidget widget;
private final AtomicLong initProjectTime = new AtomicLong(0);
private final AtomicLong initTotalTime = new AtomicLong(0);
private LocalDate lastDate = LocalDate.now();
private final SafeStopWatch projectStopWatch = SafeStopWatch.createStopped();

private LocalDate lastDate = LocalDate.now();
private ScheduledFuture<?> ticker;

public TimeTrackerWidgetService(Project project) {
Expand Down Expand Up @@ -106,7 +109,10 @@ private void checkMidnightReset() {
initProjectTime.set(0);
initTotalTime.set(0);
projectStopWatch.reset();
GLOBAL_ADDITIONS.set(0);
GLOBAL_REMOVALS.set(0);
GLOBAL_STOP_WATCH.reset();

lastDate = currentDate;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.codeclocker.plugin.intellij.widget;

import static com.codeclocker.plugin.intellij.services.ChangesActivityTracker.GLOBAL_ADDITIONS;
import static com.codeclocker.plugin.intellij.services.ChangesActivityTracker.GLOBAL_REMOVALS;
import static com.codeclocker.plugin.intellij.services.TimeSpentPerProjectLogger.GLOBAL_STOP_WATCH;
import static org.apache.commons.lang3.StringUtils.isBlank;

Expand Down Expand Up @@ -66,27 +68,46 @@ private static void fetchTimeAndInitialize() {
return;
}

Map<String, Long> projectTimes = response.getProjects();
LOG.info("Fetched daily time for project count: " + projectTimes.size());
initializeAllProjectWidgets(projectTimes, false);
Map<String, DailyTimeHttpClient.ProjectStats> projectStats = response.getProjects();
LOG.info("Fetched daily time for project count: " + projectStats.size());
initializeAllProjectWidgets(projectStats, false);
} catch (Exception e) {
LOG.error("Error initializing timer widgets", e);
initializeAllProjectWidgets(Map.of(), false);
}
}

private static void initializeAllProjectWidgets(
Map<String, Long> projectTimes, boolean subscriptionExpired) {
long totalTime = projectTimes.values().stream().mapToLong(Long::longValue).sum();
LOG.debug("Total time across all projects: {}s", totalTime);
Map<String, DailyTimeHttpClient.ProjectStats> projectStats, boolean subscriptionExpired) {
long totalTime =
projectStats.values().stream()
.mapToLong(DailyTimeHttpClient.ProjectStats::timeSpentSeconds)
.sum();
long totalAdditions =
projectStats.values().stream().mapToLong(DailyTimeHttpClient.ProjectStats::additions).sum();
long totalRemovals =
projectStats.values().stream().mapToLong(DailyTimeHttpClient.ProjectStats::removals).sum();

LOG.debug(
"Total time across all projects: {}s, additions: {}, removals: {}",
totalTime,
totalAdditions,
totalRemovals);

// Reset the global stopwatch since the backend total already includes all accumulated time
GLOBAL_STOP_WATCH.reset();

// Initialize global VCS counters with data from backend
GLOBAL_ADDITIONS.set(totalAdditions);
GLOBAL_REMOVALS.set(totalRemovals);
LOG.debug(
"Initialized GLOBAL_ADDITIONS: {}, GLOBAL_REMOVALS: {}", totalAdditions, totalRemovals);

Project[] openProjects = ProjectManager.getInstance().getOpenProjects();
for (Project project : openProjects) {
String projectName = project.getName();
long initialSeconds = projectTimes.getOrDefault(projectName, 0L);
DailyTimeHttpClient.ProjectStats stats = projectStats.get(projectName);
long initialSeconds = stats != null ? stats.timeSpentSeconds() : 0L;

TimeTrackerWidgetService service = project.getService(TimeTrackerWidgetService.class);
if (service != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.codeclocker.plugin.intellij.widget;

import static com.codeclocker.plugin.intellij.HubHost.HUB_UI_HOST;
import static com.codeclocker.plugin.intellij.services.ChangesActivityTracker.GLOBAL_ADDITIONS;
import static com.codeclocker.plugin.intellij.services.ChangesActivityTracker.GLOBAL_REMOVALS;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import com.codeclocker.plugin.intellij.apikey.ApiKeyPersistence;
import com.codeclocker.plugin.intellij.apikey.EnterApiKeyAction;
import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.ListPopup;
import com.intellij.openapi.ui.popup.ListSeparator;
import com.intellij.openapi.ui.popup.PopupStep;
import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.Nullable;

public class TimeTrackerPopup {

private static final String OPEN_DETAILED_VIEW = "Web Dashboard →";
private static final String ADD_API_KEY = "Add API Key";

public static ListPopup create(Project project, String totalTime, String projectTime) {
List<String> items = new ArrayList<>();
items.add("Total: " + totalTime);
items.add(project.getName() + ": " + projectTime);
items.add("Committed Lines: " + getFormattedVcsChanges());
items.add(OPEN_DETAILED_VIEW);

boolean hasApiKey = isNotBlank(ApiKeyPersistence.getApiKey());
if (!hasApiKey) {
items.add(ADD_API_KEY);
}

BaseListPopupStep<String> step =
new BaseListPopupStep<>("Coding Activity Today", items) {
@Override
public boolean isSelectable(String value) {
return OPEN_DETAILED_VIEW.equals(value) || ADD_API_KEY.equals(value);
}

@Override
public PopupStep<?> onChosen(String selectedValue, boolean finalChoice) {
if (OPEN_DETAILED_VIEW.equals(selectedValue)) {
BrowserUtil.browse(HUB_UI_HOST);
} else if (ADD_API_KEY.equals(selectedValue)) {
EnterApiKeyAction.showAction();
}
return FINAL_CHOICE;
}

@Override
public boolean hasSubstep(String selectedValue) {
return false;
}

@Override
public @Nullable ListSeparator getSeparatorAbove(String value) {
return OPEN_DETAILED_VIEW.equals(value) ? new ListSeparator() : null;
}
};

return JBPopupFactory.getInstance().createListPopup(step);
}

public static String getFormattedVcsChanges() {
return String.format("+%d / -%d", GLOBAL_ADDITIONS.get(), GLOBAL_REMOVALS.get());
}
}
Loading
Loading