From 8585f5741f931e95ef4ebe90eb548684a81e370b Mon Sep 17 00:00:00 2001 From: Vyn Date: Wed, 16 Oct 2024 12:17:51 +0200 Subject: [PATCH] First commit --- .gitignore | 41 +++++++++++ .gitmodules | 3 + CMakeLists.txt | 35 +++++++++ LICENSE | 21 ++++++ README.md | 55 ++++++++++++++ external/selenite | 1 + images/presentation-1.png | Bin 0 -> 28615 bytes src/main.cpp | 147 ++++++++++++++++++++++++++++++++++++++ ui/app-window.slint | 54 ++++++++++++++ ui/countdown-view.slint | 68 ++++++++++++++++++ ui/settings-view.slint | 44 ++++++++++++ ui/state.slint | 24 +++++++ ui/utils.slint | 12 ++++ 13 files changed, 505 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 160000 external/selenite create mode 100644 images/presentation-1.png create mode 100644 src/main.cpp create mode 100644 ui/app-window.slint create mode 100644 ui/countdown-view.slint create mode 100644 ui/settings-view.slint create mode 100644 ui/state.slint create mode 100644 ui/utils.slint diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fbd8eb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# clangd cache +.cache + +# CMake build directory +build + +.slint.lua +.nvimrc diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a3ddedc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/selenite"] + path = external/selenite + url = https://codeberg.org/vyn/selenite.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d1dff4b --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.21) +project(focus LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(SLINT_FEATURE_RENDERER_SKIA ON) +set(SLINT_FEATURE_RENDERER_SOFTWARE ON) + +find_package(Slint QUIET) +if (NOT Slint_FOUND) + message("Slint could not be located in the CMake module search path. Downloading it from Git and building it locally") + include(FetchContent) + FetchContent_Declare( + Slint + GIT_REPOSITORY https://github.com/slint-ui/slint.git + # `release/1` will auto-upgrade to the latest Slint >= 1.0.0 and < 2.0.0 + # `release/1.0` will auto-upgrade to the latest Slint >= 1.0.0 and < 1.1.0 + GIT_TAG release/1.8 + SOURCE_SUBDIR api/cpp + ) + FetchContent_MakeAvailable(Slint) +endif (NOT Slint_FOUND) + +add_executable(focus src/main.cpp) +target_link_libraries(focus PRIVATE Slint::Slint) +slint_target_sources( + focus ui/app-window.slint + NAMESPACE ui + LIBRARY_PATHS selenite=${CMAKE_CURRENT_SOURCE_DIR}/external/selenite/index.slint +) + +# On Windows, copy the Slint DLL next to the application binary so that it's found. +if (WIN32) + add_custom_command(TARGET focus POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ $ COMMAND_EXPAND_LISTS) +endif() diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..242da62 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..17d5f24 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +> [!warning] +> This is a work in progress and not stable. + +# Focus + +Simple Pomodoro timer. + +![Focus](https://codeberg.org/vyn/focus/raw/branch/main/images/presentation-1.png) + +## Build from source + +### Requirements + +- GCC >= 14.2.1 +- CMake >= 3.30.2 +- Ninja (or Make but you will need to adapt the commands) +- Git + +### Steps + +Fetch and setup the repository: +``` +git clone https://codeberg.org/vyn/focus.git +cd focus +git submodule update --init --recursive +``` + +To build: +``` +cmake -DCMAKE_BUILD_TYPE=Release -S . -B ./build -G Ninja +cd build +ninja +``` +Then you should have a `focus` executable in the `build` directory you are currently in. + +## Contributing + +Feel free to make suggestions and report issues, but I do **not** accept contributions (pull requests). + +## License + +Copyright (C) Vyn 2024 + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, under version 3 of the License only. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program in the LICENSE file. +If not, see . diff --git a/external/selenite b/external/selenite new file mode 160000 index 0000000..1774720 --- /dev/null +++ b/external/selenite @@ -0,0 +1 @@ +Subproject commit 1774720377afc932cb92303516e049f66d8bf7a0 diff --git a/images/presentation-1.png b/images/presentation-1.png new file mode 100644 index 0000000000000000000000000000000000000000..397d46b6ff7020d5bf401ecd7703589d9d1355a8 GIT binary patch literal 28615 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

AS@LQXI8b5EHG#FS|^*T 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

$#Bk0debCOPk#3jeP zes3`zNu#&eB}jv4zG^ccNC28EusKMGSg}Y;(#>~B1jMqqhtT-uMP`oad!#)#-BC(8 zIoaL1eY*_havb3>ugmUn3X7JYjVOfIP)onK*iV;-aU2hI+N#)Ct2^vKqnYjZ`}4E2 zp%il$mF;e~83w3`(%n4rD*$3`Gv9S_z-SAd)cOU`qT{?gPLgF93aKjPnfv$89y}N? zMsE#5sv5on@aa(Z+>6V&-Pv}h97M&v(#73ky&~=+DkL(KM{7k9%~3}XiOinSMTBnd zL5$Lx-*R^t3Xt=jBRpf4Mp$MV0TM-(gP2)$GqbTy5gCiBwlhI>6N_cLk}guTxS5+x z?n5amGH&ZI6cwqGv2R6Rf9;ldo%rE))epH=>P~h&v3Or!(`S>Me9|Ga-jzIK6f2 zG`<2erj~y^!17tqJ|f?$Ww;zJxZiVB0Ml@gLZ=1QQ)@jxKc}*~L%7}TNJfLN!y_9i zjtU=C`k6!)Cn?)_hNuFBnL}Y_sO=U-*%U%<-x}OJqN02DjE&P&%{_gHM*l=b9mi2F zFU~Kv+ug=}d$NPDy2ZoN-IXOgD_lYzLQvtD+*IOWVaXsyNZanIq&YHlqnm?pw^D1R zLqy_fRn4YaFZP$2byV36gO(DBRx>+Hle-%{I!5EskkKO0tPRqrlPMB*s;au()<}+f za{*`;AYcnyBOwe4Hqy5B^0`V?HqVbkgnUV|i9B;R|j#SdkRk8(D-)Lj%~{4bXBV!PCJkC}el1yFjX0XMswT5X6cl z29ulHWECyRIM&*!wQ%~{`uN>*A?VUG-~~d!>r{tf6QOqm$F=%2j?*|Y2$W!2k_=;G zV1r{r@QHCFqK6H+LQONyg`Kw)kV|GCC^?nd;|gg6&S`fM12AtAZGJtjSaXE%Safzy zr=?ox4o8N{WE+>=wrNj;Sgxr^1YSaQ@a*EupYm}$!2R{4Zc3S(ZCKqRb_oi()|yTV zP}R+5BO>Ou*=$8<)ozV@5jnrOI2;a_)1{dm4u`V`51xMdZ%^y3n>TL;yP>LrNLN8A zjv9fWyDJ5-MzWd1!(F;pI6`J!h7zY*ty%>IyVDydCnv+O-EOxRXXpE~i}7+Yvs%5B z&9KqvtGL{ck2)mkqu_xJDLKe>1B=FOYjZ8G89*kzKU zXp^+_b6bT_uqct!qMF{meLJ4I)va;@U}g`VIV)w@mf_~@TZkN%xgyx;U||Gs69Foq znKds(NOv(a(5sn><_zQry0SP83NhGrJKTEB?Wo#0$AfXKk#Htb)4{4$ua_4Wkv-nX zu;tbTX2zKtceIKpv{6W;L@5s!X(KK!2OVCWGIJgOAB&w7m1C-yk^lez4rN$LW=%~1 eDgXcg2mk;800000(o>TF0000 + + + +class CountdownState { + public: + CountdownState(slint::ComponentHandle ui) : ui_(ui) { + ui_->global().set_break_countdown_duration(60 * 5); + ui_->global().set_focus_countdown_duration(60 * 25); + ui_->global().set_max_session_count(4); + reset(); + } + + ui::SessionStep sessionStep() const { + return ui_->global().get_sessions_step(); + } + + int currentSession() const { + return ui_->global().get_current_session(); + } + + void setCurrentSession(int newCurrentSession) const { + ui_->global().set_current_session(newCurrentSession); + } + + int maxSessions() const { + return ui_->global().get_max_session_count(); + } + + void setSessionStep(ui::SessionStep newSessionStep) { + ui_->global().set_sessions_step(newSessionStep); + } + + ui::CountdownStatus countdownStatus() const { + return ui_->global().get_countdown_status(); + } + + void setCountdownStatus(ui::CountdownStatus newCountdownStatus) { + ui_->global().set_countdown_status(newCountdownStatus); + } + + void setCountdown(int newCountdownValue) { + ui_->global().set_countdown(newCountdownValue); + } + + int countdown() const { + return ui_->global().get_countdown(); + } + + void toggleCountdown() { + if (countdownStatus() == ui::CountdownStatus::Running) { + pause(); + } else if (countdownStatus() == ui::CountdownStatus::Paused || countdownStatus() == ui::CountdownStatus::NotStarted) { + start(); + } + } + + void goNextStep() { + if (sessionStep() == ui::SessionStep::Setup) { + setSessionStep(ui::SessionStep::Focus); + setCountdownStatus(ui::CountdownStatus::NotStarted); + setCountdown(ui_->global().get_focus_countdown_duration()); + setCurrentSession(currentSession() + 1); + } else if (sessionStep() == ui::SessionStep::Focus) { + setSessionStep(ui::SessionStep::Break); + setCountdownStatus(ui::CountdownStatus::NotStarted); + setCountdown(ui_->global().get_break_countdown_duration()); + } else if (sessionStep() == ui::SessionStep::Break) { + setCurrentSession(currentSession() + 1); + if (currentSession() < maxSessions()) { + setSessionStep(ui::SessionStep::Focus); + setCountdownStatus(ui::CountdownStatus::NotStarted); + setCountdown(ui_->global().get_focus_countdown_duration()); + } else { + setSessionStep(ui::SessionStep::Finished); + } + } + } + + void start() { + setCountdownStatus(ui::CountdownStatus::Running); + timer_.start(slint::TimerMode::Repeated, std::chrono::milliseconds(1000), [&]{ + onTimerTick(&timer_, ui_); + }); + } + + void pause() { + setCountdownStatus(ui::CountdownStatus::Paused); + timer_.stop(); + } + + void reset() { + timer_.stop(); + setSessionStep(ui::SessionStep::Setup); + setCountdownStatus(ui::CountdownStatus::NotStarted); + setCountdown(ui_->global().get_focus_countdown_duration()); + setCurrentSession(0); + } + + void onTimerTick(slint::Timer* timer, slint::ComponentHandle ui) { + int countdown = ui->global().get_countdown(); + countdown--; + ui->global().set_countdown(countdown); + + if (countdown == 0) { + timer->stop(); + ui->global().set_countdown_status(ui::CountdownStatus::NotStarted); + // TODO make it customizable + system("notify-send -t 0 \"Pomodoro\" \"task\" &"); + system("paplay /usr/share/sounds/freedesktop/stereo/complete.oga &"); + + goNextStep(); + } + } + + private: + + slint::ComponentHandle ui_; + slint::Timer timer_; +}; + +int main(int argc, char **argv) +{ + slint::ComponentHandle ui = ui::AppWindow::create(); + + CountdownState countdown(ui); + + ui->on_start_stop([&]{ + if (countdown.sessionStep() == ui::SessionStep::Setup || countdown.sessionStep() == ui::SessionStep::Finished) { + countdown.reset(); + countdown.goNextStep(); + countdown.start(); + } else if (countdown.sessionStep() == ui::SessionStep::Focus || countdown.sessionStep() == ui::SessionStep::Break) { + countdown.toggleCountdown(); + } + }); + + ui->global().on_config_changed([&] { + countdown.reset(); + }); + + ui->run(); + return 0; +} diff --git a/ui/app-window.slint b/ui/app-window.slint new file mode 100644 index 0000000..c5ae98e --- /dev/null +++ b/ui/app-window.slint @@ -0,0 +1,54 @@ +import { State, SessionStep, CountdownStatus } from "./state.slint"; +import { VText, VButton, VActionButton, Svg, Palette } from "@selenite"; +import { CountdownView } from "./countdown-view.slint"; +import { SettingsView } from "settings-view.slint"; + +enum CurrentView { + Countdown, + Settings +} + +export component AppWindow inherits Window { + title: "Focus"; + callback start-stop; + property current-view: CurrentView.Countdown; + + background: Palette.background; + default-font-size: 16px; + padding: 0px; + + VerticalLayout { + width: parent.width; + height: parent.height; + + HorizontalLayout { + padding: 8px; + vertical-stretch: 0; + alignment: start; + settingsButtons := VActionButton { + icon-svg: Svg.burger; + background: Palette.background.transparentize(1); + clicked => { + if (current-view == CurrentView.Countdown) { + current-view = CurrentView.Settings; + } else if (current-view == CurrentView.Settings) { + current-view = CurrentView.Countdown; + State.config-changed(); + } + } + } + } + if current-view == CurrentView.Countdown : countdown-view := CountdownView { + start-stop => { root.start-stop() } + vertical-stretch: 1; + padding: 32px; + padding-top: 0px; + } + if current-view == CurrentView.Settings : settings-view := SettingsView { + vertical-stretch: 1; + } + } + +} + +export { State } diff --git a/ui/countdown-view.slint b/ui/countdown-view.slint new file mode 100644 index 0000000..6f413ba --- /dev/null +++ b/ui/countdown-view.slint @@ -0,0 +1,68 @@ +import { State, SessionStep, CountdownStatus } from "./state.slint"; +import { VText, VButton, VActionButton, Svg, Palette } from "@selenite"; +import { Utils } from "utils.slint"; + +export component CountdownView inherits VerticalLayout { + callback start-stop <=> startStopButton.clicked; + + pure function format-session-step(session-step: SessionStep) -> string { + if (session-step == SessionStep.Setup) { + return "Setup"; + } + if (session-step == SessionStep.Focus) { + return "Focus"; + } + if (session-step == SessionStep.Break) { + return "Break"; + } + if (session-step == SessionStep.Finished) { + return "Finished"; + } + return "Not implemented"; + } + + VerticalLayout { + + spacing: 32px; + alignment: center; + + VerticalLayout { + VText { + font-size: 3rem; + letter-spacing: 0.2rem; + font-weight: 600; + text: "\{Utils.format-countdown(State.countdown)}"; + horizontal-alignment: center; + } + VText { + color: Palette.foreground-hint; + text: format-session-step(State.sessions-step); + horizontal-alignment: center; + } + } + HorizontalLayout { + alignment: space-between; + spacing: 8px; + for step[step-index] in State.max-session-count: Rectangle { + preferred-width: 16px; + preferred-height: 16px; + border-radius: 32px; + background: step-index + 1 == State.current-session + ? Palette.accent.transparentize(0.5) + : step-index + 1 < State.current-session ? Palette.accent + : Palette.control-background; + animate background { + duration: 0.5s; + } + } + } + HorizontalLayout { + alignment: center; + startStopButton := VActionButton { + background: Palette.foreground.transparentize(1); + icon-size: 2rem; + icon-svg: State.countdown-status != CountdownStatus.Running ? Svg.play : Svg.pause; + } + } + } +} diff --git a/ui/settings-view.slint b/ui/settings-view.slint new file mode 100644 index 0000000..9513516 --- /dev/null +++ b/ui/settings-view.slint @@ -0,0 +1,44 @@ +import { State, SessionStep, CountdownStatus } from "./state.slint"; +import { VText, VButton, VSlider, Palette } from "@selenite"; +import { Utils } from "utils.slint"; + +export component SettingsView inherits Rectangle { + VerticalLayout { + padding: 32px; + spacing: 32px; + alignment: center; + VSlider { + label: "Session duration"; + label-size: 1.25rem; + label-alignment: TextHorizontalAlignment.center; + no-background: true; + minimum: 1; + maximum: 60 * 60; + value: State.focus-countdown-duration; + format-value(value) => { return Utils.format-countdown(value); } + released(value) => { State.focus-countdown-duration = value; } + } + VSlider { + label: "Break duration"; + label-size: 1.25rem; + label-alignment: TextHorizontalAlignment.center; + no-background: true; + minimum: 1; + maximum: 60 * 60; + value: State.break-countdown-duration; + format-value(value) => { return Utils.format-countdown(value); } + released(value) => { State.break-countdown-duration = value; } + } + VSlider { + label: "Number of session"; + label-size: 1.25rem; + label-alignment: TextHorizontalAlignment.center; + no-background: true; + minimum: 1; + maximum: 8; + value: State.max-session-count; + format-value(value) => { return Math.round(value); } + released(value) => { State.max-session-count = Math.round(value); } + } + } +} diff --git a/ui/state.slint b/ui/state.slint new file mode 100644 index 0000000..8366425 --- /dev/null +++ b/ui/state.slint @@ -0,0 +1,24 @@ +export enum SessionStep { + Setup, + Focus, + Break, + Finished, +} + +export enum CountdownStatus { + Running, + Paused, + NotStarted +} + +export global State { + in-out property countdown; + in-out property focus-countdown-duration; + in-out property break-countdown-duration; + in-out property sessions-step; + in-out property countdown-status; + in-out property current-session; + in-out property max-session-count; + + callback config-changed(); +} diff --git a/ui/utils.slint b/ui/utils.slint new file mode 100644 index 0000000..9203df4 --- /dev/null +++ b/ui/utils.slint @@ -0,0 +1,12 @@ +export global Utils { + public pure function format-zero-padding(number: int) -> string { + if (number < 10) { + return "0\{number}"; + } + return number; + } + + public pure function format-countdown(countdown: int) -> string { + return "\{format-zero-padding(countdown / 60)}:\{format-zero-padding(Math.mod(countdown, 60))}"; + } +}