From 46a078d1feb7b37e6a2f941b884ef3299ca1cf45 Mon Sep 17 00:00:00 2001 From: akshay ashok Date: Sun, 15 Mar 2026 18:21:11 +0530 Subject: [PATCH] #80 Add a timer at the bottom of the IDE --- .../statusbar/PomodoroStatusBarWidget.kt | 148 ++++++++++++++++++ .../PomodoroStatusBarWidgetFactory.kt | 28 ++++ src/main/resources/META-INF/plugin.xml | 1 + 3 files changed, 177 insertions(+) create mode 100644 src/main/kotlin/com/github/akshayashokcode/devfocus/statusbar/PomodoroStatusBarWidget.kt create mode 100644 src/main/kotlin/com/github/akshayashokcode/devfocus/statusbar/PomodoroStatusBarWidgetFactory.kt diff --git a/src/main/kotlin/com/github/akshayashokcode/devfocus/statusbar/PomodoroStatusBarWidget.kt b/src/main/kotlin/com/github/akshayashokcode/devfocus/statusbar/PomodoroStatusBarWidget.kt new file mode 100644 index 0000000..34e1ecc --- /dev/null +++ b/src/main/kotlin/com/github/akshayashokcode/devfocus/statusbar/PomodoroStatusBarWidget.kt @@ -0,0 +1,148 @@ +package com.github.akshayashokcode.devfocus.statusbar + +import com.github.akshayashokcode.devfocus.services.pomodoro.PomodoroTimerService +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.StatusBar +import com.intellij.openapi.wm.StatusBarWidget +import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.util.Consumer +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.collectLatest +import org.jetbrains.annotations.NonNls +import java.awt.event.MouseEvent +import javax.swing.SwingUtilities + +class PomodoroStatusBarWidget(private val project: Project) : StatusBarWidget, StatusBarWidget.TextPresentation { + + companion object { + const val ID = "DevFocusStatusBarWidget" + } + + private val timerService = project.getService(PomodoroTimerService::class.java) + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + + private var currentText = "" + private var statusBar: StatusBar? = null + + private var timeJob: Job? = null + private var stateJob: Job? = null + private var sessionJob: Job? = null + private var phaseJob: Job? = null + + + init { + observeTimer() + } + override fun ID(): @NonNls String = ID + + override fun getPresentation(): StatusBarWidget.WidgetPresentation = this + + override fun install(statusBar: StatusBar) { + this.statusBar = statusBar + } + + override fun dispose() { + timeJob?.cancel() + stateJob?.cancel() + sessionJob?.cancel() + phaseJob?.cancel() + scope.cancel() + } + override fun getText(): String = currentText + + override fun getAlignment(): Float = 0.5f + + override fun getTooltipText(): String { + val state = timerService.state.value + val phase = timerService.currentPhase.value + val session = timerService.currentSession.value + val settings = timerService.getSettings() + + return when (state) { + PomodoroTimerService.TimerState.IDLE -> "DevFocus - Click to open" + PomodoroTimerService.TimerState.RUNNING -> { + if (phase == PomodoroTimerService.TimerPhase.WORK) { + "Work Session $session/${settings.sessionsPerRound} - Running" + } else { + "Break Time - Running" + } + } + PomodoroTimerService.TimerState.PAUSED -> { + if (phase == PomodoroTimerService.TimerPhase.WORK) { + "Work Session $session/${settings.sessionsPerRound} - Paused" + } else { + "Break Time - Paused" + } + } + } + } + + override fun getClickConsumer(): Consumer? { + return Consumer { event -> + if (event.button == MouseEvent.BUTTON1) { + // Left click - open focus tool window + SwingUtilities.invokeLater { + val toolWindowManager = ToolWindowManager.getInstance(project) + val toolWindow = toolWindowManager.getToolWindow("DevFocus") + toolWindow?.show() + } + } + } + } + + private fun observeTimer() { + timeJob = scope.launch { + timerService.timeLeft.collectLatest { + updateText() + } + } + + stateJob = scope.launch { + timerService.state.collectLatest { + updateText() + } + } + + sessionJob = scope.launch { + timerService.currentSession.collectLatest { + updateText() + } + } + + phaseJob = scope.launch { + timerService.currentPhase.collectLatest { + updateText() + } + } + } + + private fun updateText() { + val state = timerService.state.value + val phase = timerService.currentPhase.value + val time = timerService.timeLeft.value + val session = timerService.currentSession.value + val settings = timerService.getSettings() + + // Only show text when timer is active (running or paused) + val isActive = state == PomodoroTimerService.TimerState.RUNNING || + state == PomodoroTimerService.TimerState.PAUSED + + currentText = if (isActive) { + // Use stopwatch for work, coffee for break + val prefix = if (phase == PomodoroTimerService.TimerPhase.WORK) "⏱\uFE0F" else "☕" + val sessionInfo = if (phase == PomodoroTimerService.TimerPhase.WORK) { + " | Session $session/${settings.sessionsPerRound}" + } else { + " | Break" + } + "$prefix $time$sessionInfo" + } else { + "" // Empty string when idle - Widget still exists but shows nothing + } + + SwingUtilities.invokeLater { + statusBar?.updateWidget(ID) + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/akshayashokcode/devfocus/statusbar/PomodoroStatusBarWidgetFactory.kt b/src/main/kotlin/com/github/akshayashokcode/devfocus/statusbar/PomodoroStatusBarWidgetFactory.kt new file mode 100644 index 0000000..dd69594 --- /dev/null +++ b/src/main/kotlin/com/github/akshayashokcode/devfocus/statusbar/PomodoroStatusBarWidgetFactory.kt @@ -0,0 +1,28 @@ +package com.github.akshayashokcode.devfocus.statusbar + +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.NlsContexts +import com.intellij.openapi.wm.StatusBar +import com.intellij.openapi.wm.StatusBarWidget +import com.intellij.openapi.wm.StatusBarWidgetFactory +import org.jetbrains.annotations.NonNls + +class PomodoroStatusBarWidgetFactory : StatusBarWidgetFactory { + override fun getId(): @NonNls String = PomodoroStatusBarWidget.ID + + override fun getDisplayName(): @NlsContexts.ConfigurableName String = "DevFocus Timer" + + override fun isAvailable(project: Project): Boolean = true + + override fun createWidget(project: Project): StatusBarWidget { + return PomodoroStatusBarWidget(project) + } + + override fun disposeWidget(widget: StatusBarWidget) { + widget.dispose() + } + + override fun canBeEnabledOn(statusBar: StatusBar): Boolean = true + + override fun isEnabledByDefault(): Boolean = true +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e36d085..eba46b2 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -34,5 +34,6 @@ +