First commit

This commit is contained in:
Vyn 2024-10-16 12:17:51 +02:00
commit 8585f5741f
13 changed files with 505 additions and 0 deletions

41
.gitignore vendored Normal file
View file

@ -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

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "external/selenite"]
path = external/selenite
url = https://codeberg.org/vyn/selenite.git

35
CMakeLists.txt Normal file
View file

@ -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 $<TARGET_RUNTIME_DLLS:focus> $<TARGET_FILE_DIR:focus> COMMAND_EXPAND_LISTS)
endif()

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) <year> <copyright holders>
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.

55
README.md Normal file
View file

@ -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 <https://www.gnu.org/licenses/gpl-3.0.txt>.

1
external/selenite vendored Submodule

@ -0,0 +1 @@
Subproject commit 1774720377afc932cb92303516e049f66d8bf7a0

BIN
images/presentation-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

147
src/main.cpp Normal file
View file

@ -0,0 +1,147 @@
#include "app-window.h"
#include "slint_timer.h"
#include "slint.h"
#include <cstdlib>
class CountdownState {
public:
CountdownState(slint::ComponentHandle<ui::AppWindow> ui) : ui_(ui) {
ui_->global<ui::State>().set_break_countdown_duration(60 * 5);
ui_->global<ui::State>().set_focus_countdown_duration(60 * 25);
ui_->global<ui::State>().set_max_session_count(4);
reset();
}
ui::SessionStep sessionStep() const {
return ui_->global<ui::State>().get_sessions_step();
}
int currentSession() const {
return ui_->global<ui::State>().get_current_session();
}
void setCurrentSession(int newCurrentSession) const {
ui_->global<ui::State>().set_current_session(newCurrentSession);
}
int maxSessions() const {
return ui_->global<ui::State>().get_max_session_count();
}
void setSessionStep(ui::SessionStep newSessionStep) {
ui_->global<ui::State>().set_sessions_step(newSessionStep);
}
ui::CountdownStatus countdownStatus() const {
return ui_->global<ui::State>().get_countdown_status();
}
void setCountdownStatus(ui::CountdownStatus newCountdownStatus) {
ui_->global<ui::State>().set_countdown_status(newCountdownStatus);
}
void setCountdown(int newCountdownValue) {
ui_->global<ui::State>().set_countdown(newCountdownValue);
}
int countdown() const {
return ui_->global<ui::State>().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<ui::State>().get_focus_countdown_duration());
setCurrentSession(currentSession() + 1);
} else if (sessionStep() == ui::SessionStep::Focus) {
setSessionStep(ui::SessionStep::Break);
setCountdownStatus(ui::CountdownStatus::NotStarted);
setCountdown(ui_->global<ui::State>().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<ui::State>().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<ui::State>().get_focus_countdown_duration());
setCurrentSession(0);
}
void onTimerTick(slint::Timer* timer, slint::ComponentHandle<ui::AppWindow> ui) {
int countdown = ui->global<ui::State>().get_countdown();
countdown--;
ui->global<ui::State>().set_countdown(countdown);
if (countdown == 0) {
timer->stop();
ui->global<ui::State>().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::AppWindow> ui_;
slint::Timer timer_;
};
int main(int argc, char **argv)
{
slint::ComponentHandle<ui::AppWindow> 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<ui::State>().on_config_changed([&] {
countdown.reset();
});
ui->run();
return 0;
}

54
ui/app-window.slint Normal file
View file

@ -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 <CurrentView> 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 }

68
ui/countdown-view.slint Normal file
View file

@ -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;
}
}
}
}

44
ui/settings-view.slint Normal file
View file

@ -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); }
}
}
}

24
ui/state.slint Normal file
View file

@ -0,0 +1,24 @@
export enum SessionStep {
Setup,
Focus,
Break,
Finished,
}
export enum CountdownStatus {
Running,
Paused,
NotStarted
}
export global State {
in-out property <int> countdown;
in-out property <int> focus-countdown-duration;
in-out property <int> break-countdown-duration;
in-out property <SessionStep> sessions-step;
in-out property <CountdownStatus> countdown-status;
in-out property <int> current-session;
in-out property <int> max-session-count;
callback config-changed();
}

12
ui/utils.slint Normal file
View file

@ -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))}";
}
}