First commit
This commit is contained in:
commit
8585f5741f
13 changed files with 505 additions and 0 deletions
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal 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
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "external/selenite"]
|
||||
path = external/selenite
|
||||
url = https://codeberg.org/vyn/selenite.git
|
35
CMakeLists.txt
Normal file
35
CMakeLists.txt
Normal 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
21
LICENSE
Normal 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
55
README.md
Normal file
|
@ -0,0 +1,55 @@
|
|||
> [!warning]
|
||||
> This is a work in progress and not stable.
|
||||
|
||||
# Focus
|
||||
|
||||
Simple Pomodoro timer.
|
||||
|
||||

|
||||
|
||||
## 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
1
external/selenite
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 1774720377afc932cb92303516e049f66d8bf7a0
|
BIN
images/presentation-1.png
Normal file
BIN
images/presentation-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
147
src/main.cpp
Normal file
147
src/main.cpp
Normal 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
54
ui/app-window.slint
Normal 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
68
ui/countdown-view.slint
Normal 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
44
ui/settings-view.slint
Normal 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
24
ui/state.slint
Normal 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
12
ui/utils.slint
Normal 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))}";
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue