
Light-weight local project (task) minute timer(s)
Written in C++ for Linux Ubuntu (Cinnamon)
Contribution(s): Claude 4.5
Download
ptmr-343025.zip - 343.025 - 44.0kb
SHA256
da584d79bae3e165f5d2fdfa00753872
dbfed118a792f681df3119714586ea69
Changelog
343025 - percentage bg-color
342025 - up/down row adjustment
340025 - tab/focus limit
339025 - release
Install
1. unpack to location
2. create new desktop launcher (Linux)
3. name: ptmr
4. command: [location]/ptmr/ptmr-[version]
5. icon (click): alarm-symbolic (optional)
Source
- // project task timer
- // 343025
- // vgmlr
- #include <gtkmm.h>
- #include <libnotify/notify.h>
- #include <iostream>
- #include <iomanip>
- #include <sstream>
- #include <vector>
- const std::string CSS =
- "window { background-color: #EEEEEE; }"
- "list, list row, list row:hover, list row:selected, list row:focus { background-color: transparent; outline: none; box-shadow: none; }"
- "list row:first-child { margin-top: 2px; }"
- "entry { border-color: #BBBBBB; }"
- "entry:focus { border-color: #BBBBBB; }"
- ".atn-btn, .atn-btn:active { border-color: #BBBBBB; background-color: #FFFFFF; min-width: 12px; }"
- ".atn-btn:hover { background-color: #EEEEEE; }"
- ".add-btn:hover { border-color: #BBBBBB; background-color:#FFFFFF; }"
- "spinbutton button { border-color: #BBBBBB; }"
- "spinbutton button:hover { background-color: #EEEEEE; }"
- ".start-btn { border-color: #BBBBBB; min-width: 50px; }"
- ".start-btn:hover { border-color: #BBBBBB; min-width: 50px; background-color:#EEEEEE; }"
- ".remove-btn { border-color: #BBBBBB; min-width: 12px; }"
- ".remove-btn:hover { border-color: #BBBBBB; min-width: 12px; background-color:#EEEEEE; }";
- class NotificationHelper {
- public:
- NotificationHelper() {
- notify_init("ptmr");
- }
- void send_alert(const std::string& project_name, const std::string& next_project_name) {
- NotifyNotification* n = notify_notification_new(
- ("Finished: " + project_name).c_str(),
- ("Next: " + next_project_name).c_str(),
- "dialog-information"
- );
- notify_notification_set_urgency(n, NOTIFY_URGENCY_CRITICAL);
- notify_notification_show(n, nullptr);
- g_object_unref(G_OBJECT(n));
- }
- };
- class tmr_item : public Gtk::ListBoxRow {
- public:
- sigc::signal<void, tmr_item*> signal_start_clicked;
- sigc::signal<void, tmr_item*> signal_finished;
- sigc::signal<void, tmr_item*, bool> signal_move;
- sigc::signal<void, tmr_item*> signal_request_next_entry;
- sigc::signal<void, tmr_item*> signal_request_prev_entry;
- tmr_item(const std::string& name) :
- sec_left(0), sec_start(0), is_running(false), p_box(Gtk::ORIENTATION_HORIZONTAL, 5) {
- p_box.set_margin_top(5);
- p_box.set_margin_start(5);
- p_box.set_margin_end(5);
- add(p_box);
- up_btn.set_label("▲");
- up_btn.get_style_context()->add_class("atn-btn");
- up_btn.set_can_focus(false);
- up_btn.signal_clicked().connect([this] { signal_move.emit(this, true); });
- p_box.pack_start(up_btn, false, false, 0);
- down_btn.set_label("▼");
- down_btn.get_style_context()->add_class("atn-btn");
- down_btn.set_can_focus(false);
- down_btn.signal_clicked().connect([this] { signal_move.emit(this, false); });
- p_box.pack_start(down_btn, false, false, 0);
- entry.set_text(name);
- entry.signal_key_press_event().connect(sigc::mem_fun(*this, &tmr_item::on_entry_key_press));
- p_box.pack_start(entry, true, true, 0);
- auto adj = Gtk::Adjustment::create(60, 1, 360, 1);
- minutes_spin.set_adjustment(adj);
- p_box.pack_start(minutes_spin, false, false, 0);
- start_btn.set_label("Start");
- start_btn.get_style_context()->add_class("start-btn");
- start_btn.set_can_focus(false);
- start_btn.signal_clicked().connect(sigc::mem_fun(*this, &tmr_item::toggle_timer));
- p_box.pack_start(start_btn, false, false, 0);
- remove_btn.set_label("✖");
- remove_btn.get_style_context()->add_class("remove-btn");
- remove_btn.set_can_focus(false);
- remove_btn.signal_clicked().connect([this] {
- auto parent = dynamic_cast<Gtk::ListBox*>(get_parent());
- if (parent) parent->remove(*this);
- });
- p_box.pack_start(remove_btn, false, false, 0);
- show_all_children();
- }
- Gtk::Entry* get_entry() { return &entry; }
- std::string get_project_name() const { return entry.get_text(); }
- std::string get_time_string() const {
- int mins = sec_left / 60;
- int secs = sec_left % 60;
- std::stringstream ss;
- ss << std::setfill('0') << std::setw(2) << mins << ":"
- << std::setfill('0') << std::setw(2) << secs;
- return ss.str();
- }
- void update_entry_background() {
- if (sec_start <= 0) return;
-
- double percent = (sec_start - sec_left) / static_cast<double>(sec_start) * 100.0;
- percent = std::max(0.0, std::min(100.0, percent));
-
- std::stringstream ss;
- ss << std::fixed << std::setprecision(1) << percent;
- std::string percent_str = ss.str();
-
- std::string gradient = "entry { background: linear-gradient(to right, #EEEEEE 0%, #EEEEEE "
- + percent_str + "%, #FFFFFF " + percent_str + "%, #FFFFFF 100%); }";
-
- auto css_set = Gtk::CssProvider::create();
- css_set->load_from_data(gradient);
- entry.get_style_context()->add_provider(css_set, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
- }
- void toggle_timer() {
- if (!is_running) {
- if (sec_left <= 0) {
- sec_left = (int)minutes_spin.get_value() * 60;
- sec_start = sec_left;
- update_entry_background();
- }
- start_timer();
- signal_start_clicked.emit(this);
- } else {
- stop_timer();
- }
- }
- void start_timer() {
- is_running = true;
- start_btn.set_label("Pause");
- timer_conn = Glib::signal_timeout().connect(sigc::mem_fun(*this, &tmr_item::tick), 1000);
- }
- void stop_timer() {
- is_running = false;
- start_btn.set_label("Start");
- timer_conn.disconnect();
- }
- bool tick() {
- if (!is_running) return false;
- sec_left--;
- update_entry_background();
- if (sec_left <= 0) {
- stop_timer();
- start_btn.set_label("Done");
- signal_finished.emit(this);
- return false;
- }
- return true;
- }
- bool running() const { return is_running; }
- protected:
- bool on_entry_key_press(GdkEventKey* event) {
- if (event->keyval == GDK_KEY_Tab) {
- if (event->state & Gdk::SHIFT_MASK) {
- signal_request_prev_entry.emit(this);
- } else {
- signal_request_next_entry.emit(this);
- }
- return true;
- }
- return false;
- }
- private:
- int sec_left;
- int sec_start;
- bool is_running;
- sigc::connection timer_conn;
- Gtk::Box p_box;
- Gtk::Button up_btn;
- Gtk::Button down_btn;
- Gtk::Entry entry;
- Gtk::SpinButton minutes_spin;
- Gtk::Button start_btn;
- Gtk::Button remove_btn;
- };
- class PtmrApp : public Gtk::Window {
- public:
- PtmrApp() : active_item(nullptr), vbox(Gtk::ORIENTATION_VERTICAL, 10) {
- set_title("ptmr");
- set_default_size(550, 450);
- auto css_insert = Gtk::CssProvider::create();
- css_insert->load_from_data(CSS);
- Gtk::StyleContext::add_provider_for_screen(Gdk::Screen::get_default(), css_insert, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
- add(vbox);
- Gtk::HeaderBar* header = Gtk::make_managed<Gtk::HeaderBar>();
- header->set_title("ptmr");
- header->set_show_close_button(true);
- set_titlebar(*header);
- Gtk::Button* add_btn = Gtk::make_managed<Gtk::Button>("Add Project");
- add_btn->signal_clicked().connect(sigc::mem_fun(*this, &PtmrApp::on_add_project));
- header->pack_start(*add_btn);
- listbox.set_selection_mode(Gtk::SELECTION_NONE);
- scrolled_window.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
- scrolled_window.add(listbox);
- vbox.pack_start(scrolled_window, true, true, 0);
- Glib::signal_timeout().connect(sigc::mem_fun(*this, &PtmrApp::update_title), 1000);
-
- show_all_children();
- }
- private:
- void on_add_project() {
- auto item = Gtk::make_managed<tmr_item>("");
- item->signal_start_clicked.connect(sigc::mem_fun(*this, &PtmrApp::on_start_selected));
- item->signal_finished.connect(sigc::mem_fun(*this, &PtmrApp::on_timer_finished));
- item->signal_move.connect(sigc::mem_fun(*this, &PtmrApp::on_move_item));
- item->signal_request_next_entry.connect(sigc::mem_fun(*this, &PtmrApp::on_request_next_entry));
- item->signal_request_prev_entry.connect(sigc::mem_fun(*this, &PtmrApp::on_request_prev_entry));
- listbox.add(*item);
- item->show();
- }
- void on_move_item(tmr_item* item, bool move_up) {
- int current_pos = item->get_index();
- int new_pos = move_up ? current_pos - 1 : current_pos + 1;
-
- auto chidlen = listbox.get_children();
- if (new_pos >= 0 && new_pos < (int)chidlen.size()) {
- listbox.remove(*item);
- listbox.insert(*item, new_pos);
- }
- }
- void on_request_next_entry(tmr_item* current_item) {
- int current_pos = current_item->get_index();
- auto chidlen = listbox.get_children();
-
- if (current_pos + 1 < (int)chidlen.size()) {
- auto next_row = dynamic_cast<tmr_item*>(chidlen[current_pos + 1]);
- if (next_row) {
- next_row->get_entry()->grab_focus();
- }
- }
- }
- void on_request_prev_entry(tmr_item* current_item) {
- int current_pos = current_item->get_index();
-
- if (current_pos - 1 >= 0) {
- auto chidlen = listbox.get_children();
- auto prev_row = dynamic_cast<tmr_item*>(chidlen[current_pos - 1]);
- if (prev_row) {
- prev_row->get_entry()->grab_focus();
- }
- }
- }
- void on_start_selected(tmr_item* item) {
- if (active_item && active_item != item) {
- active_item->stop_timer();
- }
- active_item = item;
- }
- void on_timer_finished(tmr_item* finished_item) {
- std::string next_name = "No more projects";
- auto chidlen = listbox.get_children();
- for (size_t i = 0; i < chidlen.size(); ++i) {
- if (chidlen[i] == finished_item && i + 1 < chidlen.size()) {
- auto next_row = dynamic_cast<tmr_item*>(chidlen[i+1]);
- if (next_row) next_name = next_row->get_project_name();
- break;
- }
- }
- notifier.send_alert(finished_item->get_project_name(), next_name);
- active_item = nullptr;
- set_title("ptmr");
- }
- bool update_title() {
- if (active_item && active_item->running()) {
- set_title(active_item->get_time_string() + " - " + active_item->get_project_name());
- }
- return true;
- }
- tmr_item* active_item;
- NotificationHelper notifier;
- Gtk::Box vbox;
- Gtk::ListBox listbox;
- Gtk::ScrolledWindow scrolled_window;
- };
- int main(int argc, char* argv[]) {
- auto app = Gtk::Application::create(argc, argv, "org.gtkmm.ptmr");
- PtmrApp window;
- return app->run(window);
- }