Timer Application: C++ System Programming with CMake and PostgreSQL
Timer Application: C++ System Programming with CMake and PostgreSQL
In this post, I’ll share insights from my Timer project, which demonstrates advanced C++ system programming techniques, CMake build system integration, and PostgreSQL database connectivity for building a comprehensive time tracking application.
Project Overview
The Timer application is a sophisticated C++ system that provides comprehensive time tracking capabilities with database persistence, cross-platform compatibility, and modern C++ development practices. This project showcases system programming, database integration, and build system management.
Technical Architecture
Project Structure
Timer/
├── src/
│ ├── core/
│ │ ├── timer.cpp
│ │ ├── timer.h
│ │ ├── time_utils.cpp
│ │ ├── time_utils.h
│ │ ├── session_manager.cpp
│ │ └── session_manager.h
│ ├── database/
│ │ ├── database_manager.cpp
│ │ ├── database_manager.h
│ │ ├── session_dao.cpp
│ │ ├── session_dao.h
│ │ ├── project_dao.cpp
│ │ └── project_dao.h
│ ├── ui/
│ │ ├── console_ui.cpp
│ │ ├── console_ui.h
│ │ ├── gui_ui.cpp
│ │ └── gui_ui.h
│ ├── config/
│ │ ├── config_manager.cpp
│ │ └── config_manager.h
│ └── utils/
│ ├── logger.cpp
│ ├── logger.h
│ ├── file_utils.cpp
│ └── file_utils.h
├── include/
│ └── timer/
│ ├── timer.h
│ ├── database_manager.h
│ └── config_manager.h
├── tests/
│ ├── unit/
│ ├── integration/
│ └── fixtures/
├── scripts/
│ ├── setup_database.sql
│ ├── create_tables.sql
│ └── sample_data.sql
├── docs/
│ ├── api/
│ ├── user_guide/
│ └── developer_guide/
├── CMakeLists.txt
├── CMakeLists.txt.in
├── config.h.in
├── README.md
└── LICENSECore Implementation
Timer Core Classes
// src/core/timer.h
#ifndef TIMER_H
#define TIMER_H
#include <chrono>
#include <string>
#include <memory>
#include <vector>
#include <functional>
namespace TimerApp {
class Timer {
public:
enum class State {
STOPPED,
RUNNING,
PAUSED
};
struct Session {
std::string id;
std::string project_name;
std::string description;
std::chrono::system_clock::time_point start_time;
std::chrono::system_clock::time_point end_time;
std::chrono::duration<double> duration;
std::string tags;
bool is_active;
};
explicit Timer(const std::string& project_name = "Default");
~Timer() = default;
// Timer control
void start();
void pause();
void resume();
void stop();
void reset();
// Session management
void create_session(const std::string& project_name,
const std::string& description = "",
const std::string& tags = "");
void end_session();
void save_session();
// Getters
State get_state() const { return state_; }
std::chrono::duration<double> get_elapsed_time() const;
std::chrono::duration<double> get_total_time() const;
const Session& get_current_session() const { return current_session_; }
const std::vector<Session>& get_sessions() const { return sessions_; }
// Configuration
void set_project_name(const std::string& project_name);
void set_description(const std::string& description);
void set_tags(const std::string& tags);
// Callbacks
void set_tick_callback(std::function<void(const std::chrono::duration<double>&)> callback);
void set_session_end_callback(std::function<void(const Session&)> callback);
// Statistics
struct Statistics {
std::chrono::duration<double> total_time;
std::chrono::duration<double> average_session_time;
size_t total_sessions;
size_t active_sessions;
std::string most_used_project;
};
Statistics get_statistics() const;
private:
State state_;
Session current_session_;
std::vector<Session> sessions_;
std::chrono::system_clock::time_point start_time_;
std::chrono::system_clock::time_point pause_time_;
std::chrono::duration<double> total_paused_time_;
// Callbacks
std::function<void(const std::chrono::duration<double>&)> tick_callback_;
std::function<void(const Session&)> session_end_callback_;
// Helper methods
void update_current_session();
std::string generate_session_id() const;
void notify_tick();
void notify_session_end();
};
} // namespace TimerApp
#endif // TIMER_H
// src/core/timer.cpp
#include "timer.h"
#include "logger.h"
#include <random>
#include <sstream>
#include <iomanip>
namespace TimerApp {
Timer::Timer(const std::string& project_name)
: state_(State::STOPPED)
, total_paused_time_(std::chrono::duration<double>::zero()) {
current_session_.project_name = project_name;
current_session_.is_active = false;
Logger::get_instance().info("Timer initialized for project: " + project_name);
}
void Timer::start() {
if (state_ == State::RUNNING) {
Logger::get_instance().warning("Timer is already running");
return;
}
start_time_ = std::chrono::system_clock::now();
state_ = State::RUNNING;
// Initialize or update current session
if (!current_session_.is_active) {
create_session(current_session_.project_name,
current_session_.description,
current_session_.tags);
}
current_session_.start_time = start_time_;
current_session_.is_active = true;
Logger::get_instance().info("Timer started for project: " + current_session_.project_name);
}
void Timer::pause() {
if (state_ != State::RUNNING) {
Logger::get_instance().warning("Timer is not running, cannot pause");
return;
}
pause_time_ = std::chrono::system_clock::now();
state_ = State::PAUSED;
Logger::get_instance().info("Timer paused");
}
void Timer::resume() {
if (state_ != State::PAUSED) {
Logger::get_instance().warning("Timer is not paused, cannot resume");
return;
}
auto resume_time = std::chrono::system_clock::now();
total_paused_time_ += resume_time - pause_time_;
state_ = State::RUNNING;
Logger::get_instance().info("Timer resumed");
}
void Timer::stop() {
if (state_ == State::STOPPED) {
Logger::get_instance().warning("Timer is already stopped");
return;
}
auto end_time = std::chrono::system_clock::now();
state_ = State::STOPPED;
// Update current session
current_session_.end_time = end_time;
current_session_.duration = end_time - start_time_ - total_paused_time_;
current_session_.is_active = false;
// Save session
sessions_.push_back(current_session_);
Logger::get_instance().info("Timer stopped. Session duration: " +
std::to_string(current_session_.duration.count()) + " seconds");
// Notify callbacks
notify_session_end();
// Reset for next session
total_paused_time_ = std::chrono::duration<double>::zero();
}
void Timer::reset() {
state_ = State::STOPPED;
total_paused_time_ = std::chrono::duration<double>::zero();
current_session_ = Session{};
Logger::get_instance().info("Timer reset");
}
void Timer::create_session(const std::string& project_name,
const std::string& description,
const std::string& tags) {
current_session_.id = generate_session_id();
current_session_.project_name = project_name;
current_session_.description = description;
current_session_.tags = tags;
current_session_.is_active = false;
Logger::get_instance().info("Session created: " + current_session_.id);
}
void Timer::end_session() {
if (state_ == State::RUNNING) {
stop();
}
}
void Timer::save_session() {
if (current_session_.is_active) {
Logger::get_instance().warning("Cannot save active session");
return;
}
sessions_.push_back(current_session_);
Logger::get_instance().info("Session saved: " + current_session_.id);
}
std::chrono::duration<double> Timer::get_elapsed_time() const {
if (state_ == State::STOPPED) {
return std::chrono::duration<double>::zero();
}
auto current_time = std::chrono::system_clock::now();
auto elapsed = current_time - start_time_ - total_paused_time_;
if (state_ == State::PAUSED) {
elapsed -= (current_time - pause_time_);
}
return elapsed;
}
std::chrono::duration<double> Timer::get_total_time() const {
auto total = std::chrono::duration<double>::zero();
for (const auto& session : sessions_) {
total += session.duration;
}
// Add current session if active
if (current_session_.is_active) {
total += get_elapsed_time();
}
return total;
}
void Timer::set_tick_callback(std::function<void(const std::chrono::duration<double>&)> callback) {
tick_callback_ = callback;
}
void Timer::set_session_end_callback(std::function<void(const Session&)> callback) {
session_end_callback_ = callback;
}
Timer::Statistics Timer::get_statistics() const {
Statistics stats{};
stats.total_time = get_total_time();
stats.total_sessions = sessions_.size();
stats.active_sessions = std::count_if(sessions_.begin(), sessions_.end(),
[](const Session& s) { return s.is_active; });
if (!sessions_.empty()) {
stats.average_session_time = stats.total_time / stats.total_sessions;
// Find most used project
std::map<std::string, int> project_counts;
for (const auto& session : sessions_) {
project_counts[session.project_name]++;
}
auto most_used = std::max_element(project_counts.begin(), project_counts.end(),
[](const auto& a, const auto& b) {
return a.second < b.second;
});
if (most_used != project_counts.end()) {
stats.most_used_project = most_used->first;
}
}
return stats;
}
std::string Timer::generate_session_id() const {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(1000, 9999);
std::stringstream ss;
ss << "session_" << time_t << "_" << dis(gen);
return ss.str();
}
void Timer::notify_tick() {
if (tick_callback_) {
tick_callback_(get_elapsed_time());
}
}
void Timer::notify_session_end() {
if (session_end_callback_) {
session_end_callback_(current_session_);
}
}
} // namespace TimerApp
Database Integration
// src/database/database_manager.h
#ifndef DATABASE_MANAGER_H
#define DATABASE_MANAGER_H
#include <pqxx/pqxx>
#include <memory>
#include <string>
#include <vector>
#include "timer.h"
namespace TimerApp {
class DatabaseManager {
public:
struct ConnectionConfig {
std::string host = "localhost";
int port = 5432;
std::string database_name = "timer_db";
std::string username = "timer_user";
std::string password = "timer_pass";
int connection_timeout = 30;
};
explicit DatabaseManager(const ConnectionConfig& config);
~DatabaseManager();
// Connection management
bool connect();
void disconnect();
bool is_connected() const;
// Session management
bool save_session(const Timer::Session& session);
bool update_session(const Timer::Session& session);
bool delete_session(const std::string& session_id);
std::vector<Timer::Session> get_sessions(const std::string& project_name = "");
Timer::Session get_session(const std::string& session_id);
// Project management
bool create_project(const std::string& name, const std::string& description = "");
bool delete_project(const std::string& name);
std::vector<std::string> get_projects();
// Statistics
struct DatabaseStatistics {
size_t total_sessions;
size_t total_projects;
std::chrono::duration<double> total_time;
std::string most_active_project;
std::chrono::system_clock::time_point first_session;
std::chrono::system_clock::time_point last_session;
};
DatabaseStatistics get_statistics();
// Backup and restore
bool backup_database(const std::string& backup_file);
bool restore_database(const std::string& backup_file);
private:
ConnectionConfig config_;
std::unique_ptr<pqxx::connection> connection_;
// Helper methods
std::string build_connection_string() const;
Timer::Session session_from_row(const pqxx::row& row);
bool execute_query(const std::string& query);
bool table_exists(const std::string& table_name);
void create_tables();
};
} // namespace TimerApp
#endif // DATABASE_MANAGER_H
// src/database/database_manager.cpp
#include "database_manager.h"
#include "logger.h"
#include <sstream>
#include <iomanip>
namespace TimerApp {
DatabaseManager::DatabaseManager(const ConnectionConfig& config)
: config_(config) {
Logger::get_instance().info("DatabaseManager initialized");
}
DatabaseManager::~DatabaseManager() {
disconnect();
}
bool DatabaseManager::connect() {
try {
std::string connection_string = build_connection_string();
connection_ = std::make_unique<pqxx::connection>(connection_string);
if (connection_->is_open()) {
Logger::get_instance().info("Connected to PostgreSQL database");
// Create tables if they don't exist
create_tables();
return true;
} else {
Logger::get_instance().error("Failed to connect to database");
return false;
}
} catch (const std::exception& e) {
Logger::get_instance().error("Database connection error: " + std::string(e.what()));
return false;
}
}
void DatabaseManager::disconnect() {
if (connection_ && connection_->is_open()) {
connection_->close();
Logger::get_instance().info("Disconnected from database");
}
}
bool DatabaseManager::is_connected() const {
return connection_ && connection_->is_open();
}
bool DatabaseManager::save_session(const Timer::Session& session) {
if (!is_connected()) {
Logger::get_instance().error("Not connected to database");
return false;
}
try {
pqxx::work transaction(*connection_);
std::string query = R"(
INSERT INTO sessions (id, project_name, description, start_time, end_time,
duration_seconds, tags, is_active)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (id) DO UPDATE SET
project_name = EXCLUDED.project_name,
description = EXCLUDED.description,
start_time = EXCLUDED.start_time,
end_time = EXCLUDED.end_time,
duration_seconds = EXCLUDED.duration_seconds,
tags = EXCLUDED.tags,
is_active = EXCLUDED.is_active
)";
auto start_time_t = std::chrono::system_clock::to_time_t(session.start_time);
auto end_time_t = std::chrono::system_clock::to_time_t(session.end_time);
transaction.exec_params(query,
session.id,
session.project_name,
session.description,
start_time_t,
end_time_t,
session.duration.count(),
session.tags,
session.is_active);
transaction.commit();
Logger::get_instance().info("Session saved: " + session.id);
return true;
} catch (const std::exception& e) {
Logger::get_instance().error("Failed to save session: " + std::string(e.what()));
return false;
}
}
std::vector<Timer::Session> DatabaseManager::get_sessions(const std::string& project_name) {
std::vector<Timer::Session> sessions;
if (!is_connected()) {
Logger::get_instance().error("Not connected to database");
return sessions;
}
try {
pqxx::work transaction(*connection_);
std::string query = "SELECT * FROM sessions";
if (!project_name.empty()) {
query += " WHERE project_name = $1";
}
query += " ORDER BY start_time DESC";
pqxx::result result;
if (!project_name.empty()) {
result = transaction.exec_params(query, project_name);
} else {
result = transaction.exec(query);
}
for (const auto& row : result) {
sessions.push_back(session_from_row(row));
}
Logger::get_instance().info("Retrieved " + std::to_string(sessions.size()) + " sessions");
} catch (const std::exception& e) {
Logger::get_instance().error("Failed to retrieve sessions: " + std::string(e.what()));
}
return sessions;
}
Timer::Session DatabaseManager::get_session(const std::string& session_id) {
Timer::Session session;
if (!is_connected()) {
Logger::get_instance().error("Not connected to database");
return session;
}
try {
pqxx::work transaction(*connection_);
std::string query = "SELECT * FROM sessions WHERE id = $1";
pqxx::result result = transaction.exec_params(query, session_id);
if (!result.empty()) {
session = session_from_row(result[0]);
}
} catch (const std::exception& e) {
Logger::get_instance().error("Failed to retrieve session: " + std::string(e.what()));
}
return session;
}
bool DatabaseManager::create_project(const std::string& name, const std::string& description) {
if (!is_connected()) {
Logger::get_instance().error("Not connected to database");
return false;
}
try {
pqxx::work transaction(*connection_);
std::string query = R"(
INSERT INTO projects (name, description, created_at)
VALUES ($1, $2, NOW())
ON CONFLICT (name) DO NOTHING
)";
transaction.exec_params(query, name, description);
transaction.commit();
Logger::get_instance().info("Project created: " + name);
return true;
} catch (const std::exception& e) {
Logger::get_instance().error("Failed to create project: " + std::string(e.what()));
return false;
}
}
std::vector<std::string> DatabaseManager::get_projects() {
std::vector<std::string> projects;
if (!is_connected()) {
Logger::get_instance().error("Not connected to database");
return projects;
}
try {
pqxx::work transaction(*connection_);
std::string query = "SELECT name FROM projects ORDER BY name";
pqxx::result result = transaction.exec(query);
for (const auto& row : result) {
projects.push_back(row[0].as<std::string>());
}
} catch (const std::exception& e) {
Logger::get_instance().error("Failed to retrieve projects: " + std::string(e.what()));
}
return projects;
}
DatabaseManager::DatabaseStatistics DatabaseManager::get_statistics() {
DatabaseStatistics stats{};
if (!is_connected()) {
Logger::get_instance().error("Not connected to database");
return stats;
}
try {
pqxx::work transaction(*connection_);
// Total sessions
pqxx::result result = transaction.exec("SELECT COUNT(*) FROM sessions");
stats.total_sessions = result[0][0].as<size_t>();
// Total projects
result = transaction.exec("SELECT COUNT(*) FROM projects");
stats.total_projects = result[0][0].as<size_t>();
// Total time
result = transaction.exec("SELECT SUM(duration_seconds) FROM sessions");
if (!result[0][0].is_null()) {
stats.total_time = std::chrono::duration<double>(result[0][0].as<double>());
}
// Most active project
result = transaction.exec(R"(
SELECT project_name, COUNT(*) as session_count
FROM sessions
GROUP BY project_name
ORDER BY session_count DESC
LIMIT 1
)");
if (!result.empty()) {
stats.most_active_project = result[0][0].as<std::string>();
}
// First and last session
result = transaction.exec("SELECT MIN(start_time), MAX(start_time) FROM sessions");
if (!result.empty() && !result[0][0].is_null()) {
stats.first_session = std::chrono::system_clock::from_time_t(result[0][0].as<time_t>());
stats.last_session = std::chrono::system_clock::from_time_t(result[0][1].as<time_t>());
}
} catch (const std::exception& e) {
Logger::get_instance().error("Failed to get statistics: " + std::string(e.what()));
}
return stats;
}
std::string DatabaseManager::build_connection_string() const {
std::stringstream ss;
ss << "host=" << config_.host
<< " port=" << config_.port
<< " dbname=" << config_.database_name
<< " user=" << config_.username
<< " password=" << config_.password
<< " connect_timeout=" << config_.connection_timeout;
return ss.str();
}
Timer::Session DatabaseManager::session_from_row(const pqxx::row& row) {
Timer::Session session;
session.id = row[0].as<std::string>();
session.project_name = row[1].as<std::string>();
session.description = row[2].as<std::string>();
session.start_time = std::chrono::system_clock::from_time_t(row[3].as<time_t>());
session.end_time = std::chrono::system_clock::from_time_t(row[4].as<time_t>());
session.duration = std::chrono::duration<double>(row[5].as<double>());
session.tags = row[6].as<std::string>();
session.is_active = row[7].as<bool>();
return session;
}
void DatabaseManager::create_tables() {
try {
pqxx::work transaction(*connection_);
// Create projects table
transaction.exec(R"(
CREATE TABLE IF NOT EXISTS projects (
name VARCHAR(255) PRIMARY KEY,
description TEXT,
created_at TIMESTAMP DEFAULT NOW()
)
)");
// Create sessions table
transaction.exec(R"(
CREATE TABLE IF NOT EXISTS sessions (
id VARCHAR(255) PRIMARY KEY,
project_name VARCHAR(255) NOT NULL,
description TEXT,
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP,
duration_seconds DOUBLE PRECISION,
tags TEXT,
is_active BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (project_name) REFERENCES projects(name)
)
)");
// Create indexes
transaction.exec("CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_name)");
transaction.exec("CREATE INDEX IF NOT EXISTS idx_sessions_start_time ON sessions(start_time)");
transaction.exec("CREATE INDEX IF NOT EXISTS idx_sessions_active ON sessions(is_active)");
transaction.commit();
Logger::get_instance().info("Database tables created successfully");
} catch (const std::exception& e) {
Logger::get_instance().error("Failed to create tables: " + std::string(e.what()));
}
}
} // namespace TimerApp
CMake Build System
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Timer VERSION 1.0.0 LANGUAGES CXX)
# Set C++ standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Compiler flags
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -DNDEBUG")
endif()
# Find required packages
find_package(PkgConfig REQUIRED)
find_package(Threads REQUIRED)
# Find PostgreSQL
find_package(PostgreSQL REQUIRED)
if(NOT PostgreSQL_FOUND)
message(FATAL_ERROR "PostgreSQL not found. Please install libpq-dev or postgresql-devel")
endif()
# Find libpqxx
pkg_check_modules(LIBPQXX REQUIRED libpqxx)
if(NOT LIBPQXX_FOUND)
message(FATAL_ERROR "libpqxx not found. Please install libpqxx-dev")
endif()
# Include directories
include_directories(${CMAKE_SOURCE_DIR}/include)
include_directories(${LIBPQXX_INCLUDE_DIRS})
# Source files
set(CORE_SOURCES
src/core/timer.cpp
src/core/time_utils.cpp
src/core/session_manager.cpp
)
set(DATABASE_SOURCES
src/database/database_manager.cpp
src/database/session_dao.cpp
src/database/project_dao.cpp
)
set(UI_SOURCES
src/ui/console_ui.cpp
src/ui/gui_ui.cpp
)
set(CONFIG_SOURCES
src/config/config_manager.cpp
)
set(UTILS_SOURCES
src/utils/logger.cpp
src/utils/file_utils.cpp
)
set(ALL_SOURCES
${CORE_SOURCES}
${DATABASE_SOURCES}
${UI_SOURCES}
${CONFIG_SOURCES}
${UTILS_SOURCES}
)
# Header files
set(HEADERS
include/timer/timer.h
include/timer/database_manager.h
include/timer/config_manager.h
)
# Create library
add_library(TimerLib STATIC ${ALL_SOURCES} ${HEADERS})
# Link libraries
target_link_libraries(TimerLib
${LIBPQXX_LIBRARIES}
Threads::Threads
)
# Link PostgreSQL
target_link_libraries(TimerLib ${PostgreSQL_LIBRARIES})
# Include directories for the library
target_include_directories(TimerLib PUBLIC
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# Create executable
add_executable(timer src/main.cpp)
target_link_libraries(timer TimerLib)
# Optional GUI executable
option(BUILD_GUI "Build GUI version" OFF)
if(BUILD_GUI)
find_package(Qt5 REQUIRED COMPONENTS Core Widgets)
add_executable(timer_gui src/gui_main.cpp)
target_link_libraries(timer_gui TimerLib Qt5::Core Qt5::Widgets)
endif()
# Testing
enable_testing()
add_subdirectory(tests)
# Installation
install(TARGETS TimerLib timer
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
install(DIRECTORY include/ DESTINATION include)
# CPack configuration
set(CPACK_PACKAGE_NAME "Timer")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A comprehensive timer application")
set(CPACK_PACKAGE_VENDOR "Safa Bayar")
set(CPACK_PACKAGE_CONTACT "bayarsafa@gmail.com")
include(CPack)
# Configuration file
configure_file(
"${CMAKE_SOURCE_DIR}/config.h.in"
"${CMAKE_BINARY_DIR}/config.h"
)
# Add configuration to include path
target_include_directories(TimerLib PRIVATE ${CMAKE_BINARY_DIR})
# Print configuration summary
message(STATUS "Timer Configuration Summary:")
message(STATUS " Version: ${PROJECT_VERSION}")
message(STATUS " C++ Standard: ${CMAKE_CXX_STANDARD}")
message(STATUS " Build Type: ${CMAKE_BUILD_TYPE}")
message(STATUS " PostgreSQL: ${PostgreSQL_VERSION}")
message(STATUS " libpqxx: ${LIBPQXX_VERSION}")
message(STATUS " GUI Build: ${BUILD_GUI}")Console User Interface
// src/ui/console_ui.cpp
#include "console_ui.h"
#include "logger.h"
#include <iostream>
#include <iomanip>
#include <sstream>
#include <algorithm>
namespace TimerApp {
ConsoleUI::ConsoleUI() : running_(false) {
Logger::get_instance().info("Console UI initialized");
}
void ConsoleUI::run() {
running_ = true;
std::cout << "=== Timer Application ===" << std::endl;
std::cout << "Commands: start, pause, resume, stop, reset, status, stats, quit" << std::endl;
std::cout << "Type 'help' for more information" << std::endl;
while (running_) {
std::cout << "\n> ";
std::string command;
std::getline(std::cin, command);
process_command(command);
}
}
void ConsoleUI::process_command(const std::string& command) {
std::istringstream iss(command);
std::string cmd;
iss >> cmd;
std::transform(cmd.begin(), cmd.end(), cmd.begin(), ::tolower);
if (cmd == "start") {
handle_start_command(iss);
} else if (cmd == "pause") {
handle_pause_command();
} else if (cmd == "resume") {
handle_resume_command();
} else if (cmd == "stop") {
handle_stop_command();
} else if (cmd == "reset") {
handle_reset_command();
} else if (cmd == "status") {
handle_status_command();
} else if (cmd == "stats") {
handle_stats_command();
} else if (cmd == "projects") {
handle_projects_command();
} else if (cmd == "sessions") {
handle_sessions_command(iss);
} else if (cmd == "help") {
handle_help_command();
} else if (cmd == "quit" || cmd == "exit") {
handle_quit_command();
} else {
std::cout << "Unknown command: " << cmd << std::endl;
std::cout << "Type 'help' for available commands" << std::endl;
}
}
void ConsoleUI::handle_start_command(std::istringstream& iss) {
std::string project_name;
std::string description;
iss >> project_name;
if (project_name.empty()) {
project_name = "Default";
}
// Get description if provided
std::string remaining;
std::getline(iss, remaining);
if (!remaining.empty()) {
description = remaining.substr(1); // Remove leading space
}
timer_.set_project_name(project_name);
timer_.set_description(description);
timer_.start();
std::cout << "Timer started for project: " << project_name << std::endl;
if (!description.empty()) {
std::cout << "Description: " << description << std::endl;
}
}
void ConsoleUI::handle_pause_command() {
timer_.pause();
std::cout << "Timer paused" << std::endl;
}
void ConsoleUI::handle_resume_command() {
timer_.resume();
std::cout << "Timer resumed" << std::endl;
}
void ConsoleUI::handle_stop_command() {
timer_.stop();
std::cout << "Timer stopped" << std::endl;
}
void ConsoleUI::handle_reset_command() {
timer_.reset();
std::cout << "Timer reset" << std::endl;
}
void ConsoleUI::handle_status_command() {
auto state = timer_.get_state();
auto elapsed = timer_.get_elapsed_time();
auto total = timer_.get_total_time();
std::cout << "\n=== Timer Status ===" << std::endl;
std::cout << "State: " << state_to_string(state) << std::endl;
std::cout << "Elapsed Time: " << format_duration(elapsed) << std::endl;
std::cout << "Total Time: " << format_duration(total) << std::endl;
const auto& session = timer_.get_current_session();
if (!session.id.empty()) {
std::cout << "Current Session: " << session.id << std::endl;
std::cout << "Project: " << session.project_name << std::endl;
if (!session.description.empty()) {
std::cout << "Description: " << session.description << std::endl;
}
}
}
void ConsoleUI::handle_stats_command() {
auto stats = timer_.get_statistics();
std::cout << "\n=== Statistics ===" << std::endl;
std::cout << "Total Time: " << format_duration(stats.total_time) << std::endl;
std::cout << "Total Sessions: " << stats.total_sessions << std::endl;
std::cout << "Active Sessions: " << stats.active_sessions << std::endl;
std::cout << "Average Session Time: " << format_duration(stats.average_session_time) << std::endl;
if (!stats.most_used_project.empty()) {
std::cout << "Most Used Project: " << stats.most_used_project << std::endl;
}
}
void ConsoleUI::handle_projects_command() {
if (!db_manager_) {
std::cout << "Database not connected" << std::endl;
return;
}
auto projects = db_manager_->get_projects();
std::cout << "\n=== Projects ===" << std::endl;
if (projects.empty()) {
std::cout << "No projects found" << std::endl;
} else {
for (const auto& project : projects) {
std::cout << "- " << project << std::endl;
}
}
}
void ConsoleUI::handle_sessions_command(std::istringstream& iss) {
std::string project_name;
iss >> project_name;
if (!db_manager_) {
std::cout << "Database not connected" << std::endl;
return;
}
auto sessions = db_manager_->get_sessions(project_name);
std::cout << "\n=== Sessions ===" << std::endl;
if (sessions.empty()) {
std::cout << "No sessions found" << std::endl;
} else {
std::cout << std::left << std::setw(20) << "ID"
<< std::setw(15) << "Project"
<< std::setw(20) << "Duration"
<< std::setw(20) << "Start Time" << std::endl;
std::cout << std::string(75, '-') << std::endl;
for (const auto& session : sessions) {
std::cout << std::left << std::setw(20) << session.id.substr(0, 19)
<< std::setw(15) << session.project_name.substr(0, 14)
<< std::setw(20) << format_duration(session.duration)
<< std::setw(20) << format_time(session.start_time) << std::endl;
}
}
}
void ConsoleUI::handle_help_command() {
std::cout << "\n=== Available Commands ===" << std::endl;
std::cout << "start [project] [description] - Start timer for project" << std::endl;
std::cout << "pause - Pause current timer" << std::endl;
std::cout << "resume - Resume paused timer" << std::endl;
std::cout << "stop - Stop current timer" << std::endl;
std::cout << "reset - Reset timer" << std::endl;
std::cout << "status - Show current status" << std::endl;
std::cout << "stats - Show statistics" << std::endl;
std::cout << "projects - List all projects" << std::endl;
std::cout << "sessions [project] - List sessions" << std::endl;
std::cout << "help - Show this help" << std::endl;
std::cout << "quit/exit - Exit application" << std::endl;
}
void ConsoleUI::handle_quit_command() {
if (timer_.get_state() == Timer::State::RUNNING) {
std::cout << "Timer is running. Stop it first or it will be saved." << std::endl;
std::cout << "Continue? (y/n): ";
char response;
std::cin >> response;
if (response == 'y' || response == 'Y') {
timer_.stop();
}
}
running_ = false;
std::cout << "Goodbye!" << std::endl;
}
std::string ConsoleUI::state_to_string(Timer::State state) {
switch (state) {
case Timer::State::STOPPED: return "Stopped";
case Timer::State::RUNNING: return "Running";
case Timer::State::PAUSED: return "Paused";
default: return "Unknown";
}
}
std::string ConsoleUI::format_duration(const std::chrono::duration<double>& duration) {
auto total_seconds = static_cast<int>(duration.count());
auto hours = total_seconds / 3600;
auto minutes = (total_seconds % 3600) / 60;
auto seconds = total_seconds % 60;
std::ostringstream oss;
oss << std::setfill('0') << std::setw(2) << hours << ":"
<< std::setw(2) << minutes << ":" << std::setw(2) << seconds;
return oss.str();
}
std::string ConsoleUI::format_time(const std::chrono::system_clock::time_point& time_point) {
auto time_t = std::chrono::system_clock::to_time_t(time_point);
auto tm = *std::localtime(&time_t);
std::ostringstream oss;
oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
return oss.str();
}
} // namespace TimerApp
Testing Framework
Unit Tests
// tests/unit/test_timer.cpp
#include <gtest/gtest.h>
#include "timer.h"
#include <thread>
#include <chrono>
class TimerTest : public ::testing::Test {
protected:
void SetUp() override {
timer = std::make_unique<TimerApp::Timer>("TestProject");
}
std::unique_ptr<TimerApp::Timer> timer;
};
TEST_F(TimerTest, InitialState) {
EXPECT_EQ(timer->get_state(), TimerApp::Timer::State::STOPPED);
EXPECT_EQ(timer->get_elapsed_time().count(), 0);
}
TEST_F(TimerTest, StartTimer) {
timer->start();
EXPECT_EQ(timer->get_state(), TimerApp::Timer::State::RUNNING);
// Wait a bit and check elapsed time
std::this_thread::sleep_for(std::chrono::milliseconds(100));
EXPECT_GT(timer->get_elapsed_time().count(), 0);
}
TEST_F(TimerTest, PauseResume) {
timer->start();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
auto elapsed_before_pause = timer->get_elapsed_time();
timer->pause();
EXPECT_EQ(timer->get_state(), TimerApp::Timer::State::PAUSED);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
auto elapsed_after_pause = timer->get_elapsed_time();
// Elapsed time should not change while paused
EXPECT_EQ(elapsed_before_pause.count(), elapsed_after_pause.count());
timer->resume();
EXPECT_EQ(timer->get_state(), TimerApp::Timer::State::RUNNING);
}
TEST_F(TimerTest, StopTimer) {
timer->start();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
timer->stop();
EXPECT_EQ(timer->get_state(), TimerApp::Timer::State::STOPPED);
auto sessions = timer->get_sessions();
EXPECT_EQ(sessions.size(), 1);
EXPECT_GT(sessions[0].duration.count(), 0);
}
TEST_F(TimerTest, ResetTimer) {
timer->start();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
timer->stop();
auto sessions_before = timer->get_sessions().size();
timer->reset();
EXPECT_EQ(timer->get_state(), TimerApp::Timer::State::STOPPED);
EXPECT_EQ(timer->get_elapsed_time().count(), 0);
EXPECT_EQ(timer->get_sessions().size(), sessions_before);
}
TEST_F(TimerTest, Statistics) {
timer->start();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
timer->stop();
auto stats = timer->get_statistics();
EXPECT_EQ(stats.total_sessions, 1);
EXPECT_GT(stats.total_time.count(), 0);
}Lessons Learned
C++ System Programming
- Modern C++: Use of C++17 features and best practices
- Memory Management: Smart pointers and RAII principles
- Thread Safety: Proper synchronization and thread management
- Error Handling: Comprehensive exception handling
Database Integration
- PostgreSQL: Advanced PostgreSQL integration with libpqxx
- Connection Management: Proper database connection handling
- Transaction Management: ACID compliance and transaction safety
- Performance: Optimized queries and indexing strategies
Build Systems
- CMake: Modern CMake practices and cross-platform builds
- Dependencies: Proper dependency management and linking
- Configuration: Build-time configuration and feature flags
- Packaging: CPack integration for distribution
Software Architecture
- Modular Design: Clean separation of concerns
- Interface Design: Well-defined APIs and abstractions
- Testing: Comprehensive unit and integration testing
- Documentation: Clear code documentation and user guides
Future Enhancements
Advanced Features
- Web Interface: REST API and web-based UI
- Mobile App: Cross-platform mobile application
- Team Features: Multi-user support and collaboration
- Reporting: Advanced reporting and analytics
Technical Improvements
- Performance: Optimized database queries and caching
- Scalability: Support for large datasets and concurrent users
- Security: Authentication and authorization
- Integration: Third-party service integrations
Conclusion
The Timer application demonstrates comprehensive C++ system programming skills and database integration expertise. Key achievements include:
- System Programming: Advanced C++ system programming techniques
- Database Integration: Comprehensive PostgreSQL integration
- Build System: Modern CMake build system implementation
- User Interface: Console-based user interface
- Testing: Comprehensive unit testing framework
- Documentation: Clear documentation and user guides
The project is available on GitHub and serves as a comprehensive example of C++ system programming and database application development.
This project represents my deep dive into C++ system programming and demonstrates how modern C++ can be used to build robust, efficient applications with database integration. The lessons learned here continue to influence my approach to system-level programming and database application development.