<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pomodoro</title>
    <style>
        :root {
            --paper: #eae7e1;
            --ink: #2b2b28;
            --ink-soft: #55524b;
            --rule: #b9b3a6;
            --accent: #6e6a5f;
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: "Courier New", Courier, monospace;
        }

        html, body {
            height: 100%;
        }

        body {
            background-color: var(--paper);
            color: var(--ink);
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 24px;
            letter-spacing: 0.02em;
        }

        #title, #state, #player {
            width: 100%;
            max-width: 480px;
        }

        .title {
            text-align: center;
            margin-bottom: 22px;
        }

        .eyebrow {
            color: var(--ink-soft);
            font-size: 0.72rem;
            text-transform: uppercase;
            letter-spacing: 0.22em;
            margin-bottom: 6px;
        }

        .wordmark {
            color: var(--ink);
            font-size: 1.9rem;
            font-weight: bold;
            letter-spacing: 0.14em;
            text-transform: uppercase;
        }

        .title-rule {
            width: 64px;
            height: 1px;
            background-color: var(--rule);
            margin: 12px auto 0;
        }

        .line {
            border: none;
            border-top: 1px solid var(--rule);
            margin: 14px 0;
            height: 0;
        }

        #state {
            border: 1px solid var(--rule);
            padding: 20px 22px;
        }

        .indicator {
            margin: 0;
            font-size: 0.92rem;
            color: var(--ink-soft);
            line-height: 1.6;
        }

        #session-type {
            color: var(--ink);
            font-weight: bold;
            text-transform: uppercase;
            letter-spacing: 0.08em;
            font-size: 0.85rem;
        }

        #time-remaining {
            color: var(--accent);
            font-size: 0.92rem;
        }

        #total-sessions, #total-time {
            font-size: 0.82rem;
        }
    </style>
</head>
<body>
    <div id="title" class="title">
        <div class="eyebrow">Temporizador de trabajo</div>
        <h1 class="wordmark">Pomodoro</h1>
        <div class="title-rule"></div>
    </div>
    <div id="state"> 
        <div id="total-sessions" class="indicator"> </div> 
        <div id="total-time" class="indicator"> </div>
        <div class="line"></div>
        <div id="session-type" class="indicator"> </div>
        <div class="line"></div>
        <div id="time-remaining" class="indicator"> </div>
    </div>
    <div id="player"> </div>
</body>
</html>

<script>
// Timer Options
const work_time=25;
const rest_time=5;
const long_rest_time=15;
const long_rest_interval=5;
const second = 1000;

// Language and Translation Options
const work_session_str = "Sesión de trabajo";
const long_rest_str = "Sesión de descanso largo";
const short_rest_str = "Sesión de descanso corto";
const total_sessions_str="Sesiones de trabajo totales";
const total_time_str = "Tiempo trabajado";
const press_ctrl_c_str = "Esperando pulsación CTRL+C";
const hours_str = "horas";
const minutes_str ="minutos";
const seconds_str ="segundos";
const and_str = "y";
const remaining_str ="restantes";

// HTML-linked labels
const session_type_tag = document.getElementById('session-type')
const total_sessions_tag = document.getElementById('total-sessions')
const total_time_tag = document.getElementById('total-time')
const time_remaining_tag = document.getElementById('time-remaining')

// Current URL for adding parameters
const url = new URL(window.location.href);

// Application Variables
let rest_counter=0;
let long_rest_counter=0;
let work_counter=0;
let is_rest_time=0;
let is_long_rest_time=0;
let total_work_minutes=0;
let work_hours =0;
let work_minutes =0
//
let seconds_counter = 0
let seconds_timer = 0
let minutes_counter = 0
let minutes_timer = 0
let hours_timer = 0
// 
let load_flag = false;

// Oscillator Settings
var context = new AudioContext();
var oscillator = context.createOscillator();
oscillator.type = "sine";
oscillator.start();
context.suspend()

/*
    Generates a beep using the previously created oscillator. 
    It is controlled by context.suspend() and context.resume()
*/
function beep (frequency, time) {
    oscillator.frequency.value = frequency;
    oscillator.connect(context.destination);
    context.resume();
    // Beep for 500 milliseconds
    setTimeout(function () {
        context.suspend()
    }, time);
}

/*
Modify the `time_remaining_tag` to set the times for each iteration. 
The main function must wait the same amount of time to synchronize.
*/
async function timer(countdown){
    let minutes_remaining;
    let seconds_remaining;
    countdown = countdown*60
    while (countdown > 0) {
        countdown--;
        minutes_remaining = Math.trunc(countdown / 60);
        seconds_remaining = Math.trunc (countdown % 60);
        time_remaining_tag.innerHTML=`${minutes_remaining} ${minutes_str} ${seconds_remaining} ${seconds_str} ${remaining_str}...`;
        await new Promise(r => setTimeout(r, second));
    }
}

/*
Two promises that call out to each other in an intermittent loop that projects a clock onto the screen.
The beep will sound every second. 
*/
async function waitingKeypress() {
    return new Promise((resolve) => {
        let stopped = false;

        function onKeyHandler(event) {
            if (event.ctrlKey && event.key.toLowerCase() === "c") {
                event.preventDefault();

                stopped = true;
                document.removeEventListener("keydown", onKeyHandler);

                context.suspend();
                resolve();
            }
        }

        document.addEventListener("keydown", onKeyHandler);
        seconds_counter = 0
        seconds_timer = 0
        minutes_counter = 0
        minutes_timer = 0
        hours_timer = 0

        async function playBeepLoop() {
            while (!stopped) {
                beep(250, second/2);
                seconds_counter++
                seconds_timer = Math.trunc(seconds_counter % 60)
                minutes_counter = Math.trunc(seconds_counter / 60)
                minutes_timer = Math.trunc(minutes_counter % 60)
                hours_timer = Math.trunc(minutes_counter / 60)

                if (seconds_timer < 10) seconds_timer = `0${seconds_timer}`
                if (minutes_timer < 10) minutes_timer = `0${minutes_timer}`
                if (hours_timer < 10) hours_timer = `0${hours_timer}`

                time_remaining_tag.innerHTML=`${press_ctrl_c_str} (${hours_timer}:${minutes_timer}:${seconds_timer})`;
                await new Promise((resolve) => {
                    setTimeout(resolve, second);
                });
            }
        }

        playBeepLoop();
    });
}

/*
    Load the session based on the values and calculate based on the work counter
*/
function loadSession() {
    const params = new URLSearchParams(window.location.search);
    work_counter = Number(params.get("work_counter")) || 0;
    rest_counter= work_counter*2;
    long_rest_counter=work_counter;
    is_rest_time=0;
    is_long_rest_time=work_counter;
    total_work_minutes=work_time*work_counter;
    work_hours = Math.trunc (total_work_minutes / 60)
    work_minutes = Math.trunc (total_work_minutes % 60)

}

/*
    Saves the work counter and updates it in the URL parameters
*/
function saveSession () {
    url.searchParams.set("work_counter", work_counter);
    window.history.replaceState({}, "", url);
}


async function main (){
    await waitingKeypress();
    var elements = document.querySelectorAll('.line');
    for(var i=0; i<elements.length; i++){
        elements[i].style.display = "block"
    }
    loadSession ()
    total_time_tag.innerHTML = `${total_time_str}: ${work_hours} ${hours_str} ${and_str} ${work_minutes} ${minutes_str}`
    while (true) {
        total_sessions_tag.innerHTML = `${total_sessions_str}: ${work_counter}`
        rest_counter++;
        is_rest_time = rest_counter%2;
        if (is_rest_time == 0) {          
            long_rest_counter++;
            is_long_rest_time = long_rest_counter % long_rest_interval;
            if (is_long_rest_time == 0) {
                session_type_tag.innerHTML=`${long_rest_str}${long_rest_time} ${minutes_str}`
                await timer (long_rest_time)
                await waitingKeypress();
            }
            else {
                session_type_tag.innerHTML=`${short_rest_str}${rest_time} ${minutes_str}`
                await timer (rest_time)
                await waitingKeypress();
            }
        }
        else {
            work_counter++
            session_type_tag.innerHTML=`${work_session_str}${work_time} ${minutes_str}`
            await timer (work_time)
            await waitingKeypress();
            saveSession ()


        }
        total_work_minutes = work_counter * work_time;
        work_hours = Math.trunc(total_work_minutes / 60)
        work_minutes = Math.trunc (total_work_minutes % 60)
        total_time_tag.innerHTML = `${total_time_str}: ${work_hours} ${hours_str} ${and_str} ${work_minutes} ${minutes_str}`
    }
}


main ()

/*
# Copyright (C) 2025 HURSZ
#
# 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, either version 3 of the License, or
# (at your option) any later version.
#
# 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. If not, see <https://www.gnu.org/licenses/>.
*/

</script>
Edit

Pub: 21 Aug 2026 20:50 UTC

Views: 11