commit 643487f4b3e539aa4fba0194c9de4df6bf4ff616 Author: Jenny Diomidova Date: Mon Dec 8 12:21:57 2025 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af03d49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ + +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +node_modules + +.venv +.lock-waf_*_build diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/assets/screenshots/chalk.png b/assets/screenshots/chalk.png new file mode 100644 index 0000000..12c2228 Binary files /dev/null and b/assets/screenshots/chalk.png differ diff --git a/assets/screenshots/diorite.png b/assets/screenshots/diorite.png new file mode 100644 index 0000000..d477ad4 Binary files /dev/null and b/assets/screenshots/diorite.png differ diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..d02e5fb --- /dev/null +++ b/flake.lock @@ -0,0 +1,190 @@ +{ + "nodes": { + "commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "pebble", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1737465171, + "narHash": "sha256-R10v2hoJRLq8jcL4syVFag7nIGE7m13qO48wRIukWNg=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "9364dc02281ce2d37a1f55b6e51f7c0f65a75f17", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "pebble", + "commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1734649271, + "narHash": "sha256-4EVBRhOjMDuGtMaofAIqzJbg4Ql7Ai0PSeuVZTHjyKQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d70bd19e0a38ad4790d3913bf08fcbfc9eeca507", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "pebble": { + "inputs": { + "commit-hooks": "commit-hooks", + "flake-compat": "flake-compat_2", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs" + }, + "locked": { + "dirtyRev": "3fe382c507afd73a2c6a51ece280c3fb567ac53a-dirty", + "dirtyShortRev": "3fe382c-dirty", + "lastModified": 1764961369, + "narHash": "sha256-TE0XcWkJNwMVKonZ8y/c4kVLEp71A+45T5Fys2Jv2uI=", + "type": "git", + "url": "file:///home/diomidova/Pebble/pebble.nix" + }, + "original": { + "type": "git", + "url": "file:///home/diomidova/Pebble/pebble.nix" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "pebble": "pebble" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..df69be9 --- /dev/null +++ b/flake.nix @@ -0,0 +1,17 @@ +{ + description = "Cryptic Time"; + + inputs = { + # pebble.url = "github:pebble-dev/pebble.nix"; + pebble.url = "github:diomidov/pebble.nix"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { pebble, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: { + devShell = pebble.pebbleEnv.${system} { + withCoreDevices = true; + }; + }); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ded4275 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,28 @@ +{ + "name": "cryptic-clock", + "version": "1.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cryptic-clock", + "version": "1.1", + "dependencies": { + "@smallstoneapps/utils": "^1.3.0", + "pebble-cpp": "^0.3.1" + } + }, + "node_modules/@smallstoneapps/utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@smallstoneapps/utils/-/utils-1.3.0.tgz", + "integrity": "sha512-2XSH7umjEtXqdBTil8yhdLRjw259ZgUhn/c6f/4vW5RlM/jo4J+Hoygc+tr4z8Wv5ymnFvxfNq2MpqTqNM+TTg==", + "license": "MIT" + }, + "node_modules/pebble-cpp": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pebble-cpp/-/pebble-cpp-0.3.1.tgz", + "integrity": "sha512-lR6/icexx5ol4JfowONUflfKofR8wl6fQB3U1b4OwH1S9Iei12UNv9s7X+gSDSsOoOLSl1fPyf6CGbooH7rxbg==", + "license": "Apache-2.0" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d34cc0f --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "cryptic-clock", + "author": "jennyaa", + "version": "1.1", + "keywords": [ + "pebble-app" + ], + "private": true, + "dependencies": { + "@smallstoneapps/utils": "^1.3.0", + "pebble-cpp": "^0.3.1" + }, + "pebble": { + "displayName": "Cryptic Time", + "uuid": "53d1d465-fafe-4e7a-8b4f-44382b32e384", + "sdkVersion": "3", + "enableMultiJS": true, + "targetPlatforms": [ + "basalt", + "chalk", + "diorite" + ], + "watchapp": { + "watchface": true + }, + "messageKeys": [ + "dummy" + ], + "resources": { + "media": [ + { + "type": "bitmap", + "name": "DIGITS", + "file": "digits.png" + }, + { + "type": "bitmap", + "name": "FRAME", + "file": "frame.png" + }, + { + "type": "bitmap", + "name": "MENU_ICON", + "file": "icon.png", + "menuIcon": true + } + ] + } + } +} diff --git a/resources/digits.png b/resources/digits.png new file mode 100644 index 0000000..7e2a67f Binary files /dev/null and b/resources/digits.png differ diff --git a/resources/frame.png b/resources/frame.png new file mode 100644 index 0000000..c95a22c Binary files /dev/null and b/resources/frame.png differ diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 0000000..db91163 Binary files /dev/null and b/resources/icon.png differ diff --git a/src/c/cryptic_clock.cpp b/src/c/cryptic_clock.cpp new file mode 100644 index 0000000..94558f6 --- /dev/null +++ b/src/c/cryptic_clock.cpp @@ -0,0 +1,72 @@ +#include +#include "pebble_bitmap_layer.hpp" +#include <@smallstoneapps/utils/macros.h> +#include "digit_layer.hpp" + +using namespace Pebble; + +#undef DEBUG + +class MainWindow : public AppWindow, public TickTimer { + DigitLayer digitLayers[4]; + GOwnedBitmap frameBitmap; + Pebble::BitmapLayer frameLayer; +protected: + void OnLoad() { + auto bounds = this->GetBounds(); + int16_t w2 = bounds.size.w / 2; + int16_t h = bounds.size.h; + + digitLayers[0].SetFrame(Pebble::GRect(w2-68, 0, 32, h)); + digitLayers[1].SetFrame(Pebble::GRect(w2-36, 0, 32, h)); + digitLayers[2].SetFrame(Pebble::GRect(w2+4, 0, 32, h)); + digitLayers[3].SetFrame(Pebble::GRect(w2+36, 0, 32, h)); + digitLayers[0].SetDigitCount(clock_is_24h_style() ? 3 : 2); + digitLayers[1].SetDigitCount(clock_is_24h_style() ? 24 : 12); + digitLayers[2].SetDigitCount(6); + digitLayers[3].SetDigitCount(10); + for (size_t i = 0; i < 4; i++) { + this->digitLayers[i].UpdateLayout(); + this->AddChild(this->digitLayers[i]); + } + + this->frameBitmap = Pebble::GOwnedBitmap::CreateWithResource(RESOURCE_ID_FRAME); + this->frameLayer.SetBitmap(this->frameBitmap); + this->frameLayer.SetFrame(bounds); + this->frameLayer.SetCompositingMode(GCompOpSet); + this->AddChild(this->frameLayer); + +#ifdef DEBUG + TickTimerStart(SECOND_UNIT); +#else + TickTimerStart(MINUTE_UNIT); +#endif + UpdateTime(false); + } + + void UpdateTime(bool animate=true) { + DateTime date = DateTime::Now(); + int hour = date.GetHour(); +#ifdef DEBUG + int minute = date.GetSecond(); +#else + int minute = date.GetMinute(); +#endif + if (!clock_is_24h_style()) hour %= 12; + this->digitLayers[0].RotateTo(hour / 10, animate); + this->digitLayers[1].RotateTo(hour, animate); + this->digitLayers[2].RotateTo(minute / 10, animate); + this->digitLayers[3].RotateTo(minute, animate); + } + + void OnTick(tm &tick_time, TimeUnits units_changed) { + UpdateTime(true); + } +}; + +MAIN { + MainWindow window; + window.StackPush(true); + app_event_loop(); + return 0; +} \ No newline at end of file diff --git a/src/c/digit_layer.cpp b/src/c/digit_layer.cpp new file mode 100644 index 0000000..306823b --- /dev/null +++ b/src/c/digit_layer.cpp @@ -0,0 +1,73 @@ +#include "digit_layer.hpp" +#include <@smallstoneapps/utils/macros.h> + +using namespace Pebble; + +namespace { + // NOTE: technically a memory leak, destructor will never be called. + // Can't make it a static variable in draw_digit, because `atexit` is broken on Pebble. + static GOwnedBitmap digitBitmaps[10]; + static bool bitmapsInitialized = false; + + static void draw_digit(const Pebble::GContext& ctx, const Pebble::GRect& bounds, const uint16_t digit) { + if (!bitmapsInitialized) { + auto bitmap = GOwnedBitmap::CreateWithResource(RESOURCE_ID_DIGITS); + auto size = bitmap.GetBounds().size; + uint16_t w = size.w / 10; + uint16_t h = size.h; + for (int i = 0; i < 10; i++) { + digitBitmaps[i] = GOwnedBitmap::CreateSubBitmap(bitmap, Pebble::GRect(i * w, 0, w, h)); + } + bitmapsInitialized = true; + } + + auto bitmap = &digitBitmaps[digit]; + auto rect = bitmap->GetBounds(); + rect.Align(bounds, GAlignCenter, false); + ctx.DrawBitmapInRect(*bitmap, rect); + } +} + +void DigitLayerInner::OnUpdate(const Pebble::GContext& ctx) { + auto bounds = this->GetBounds(); + + ctx.SetFillColor(GColorBlack); + ctx.SetCompositingMode(GCompOpSet); + for (int i = 0; i <= this->digitCount + 2 * this->extraDigits; i++) { + Pebble::GRect rect(0, i * this->digitHeight, bounds.size.w, this->digitHeight); + uint16_t digit = ((i - this->extraDigits) % this->digitCount + this->digitCount) % this->digitCount % 10; + draw_digit(ctx, rect, digit); + } +} + +DigitLayer::DigitLayer() : Pebble::SimpleLayer(Pebble::GRect()) { + this->scrollLayer.AddChild(this->innerLayer); + this->scrollLayer.SetShadowHidden(true); + this->AddChild(this->scrollLayer); +} + +void DigitLayer::UpdateLayout() { + auto bounds = this->GetBounds(); + this->scrollLayer.SetFrame(bounds); + bounds.size.h = (this->innerLayer.digitCount + 2 * this->innerLayer.extraDigits) * this->innerLayer.digitHeight; + this->innerLayer.SetFrame(bounds); + this->scrollLayer.SetContentSize(bounds.size); +} + + +int16_t DigitLayer::ComputeOffset(uint16_t digit) { + auto bounds = this->GetBounds(); + return bounds.size.h / 2 - this->innerLayer.digitHeight / 2 + - (digit + this->innerLayer.extraDigits) * this->innerLayer.digitHeight; +} + +void DigitLayer::RotateTo(uint16_t digit, bool animate) { + digit %= innerLayer.digitCount; + auto currentOffset = this->scrollLayer.GetContentOffset(); + if (digit == 0 && currentOffset.y <= ComputeOffset(this->innerLayer.digitCount / 2)) { + currentOffset.y += this->innerLayer.digitCount * this->innerLayer.digitHeight; + this->scrollLayer.SetContentOffset(currentOffset, false); + } + + this->scrollLayer.SetContentOffset(this->ComputeOffset(digit), animate); +} \ No newline at end of file diff --git a/src/c/digit_layer.hpp b/src/c/digit_layer.hpp new file mode 100644 index 0000000..fdd8414 --- /dev/null +++ b/src/c/digit_layer.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include <@smallstoneapps/utils/macros.h> + +class DigitLayerInner : public Pebble::CustomLayer { + static constexpr int16_t extraDigits = 2; + static constexpr int16_t digitHeight = 56; + int16_t digitCount = 10; + + DigitLayerInner() : Pebble::CustomLayer(Pebble::GRect()) {} + + void SetDigitCount(uint16_t cnt = 10) { + this->digitCount = cnt; + this->MarkDirty(); + } + + static void DrawDigit(const Pebble::GContext& ctx, const Pebble::GRect& bounds, const uint16_t digit); + + friend class DigitLayer; + +protected: + void OnUpdate(const Pebble::GContext& ctx); +}; + +class DigitLayer : public Pebble::SimpleLayer { + DigitLayerInner innerLayer; + Pebble::ScrollLayer scrollLayer; +public: + DigitLayer(); + + void SetDigitCount(uint16_t cnt = 10) { + this->innerLayer.SetDigitCount(cnt); + this->MarkDirty(); + } + void UpdateLayout(); + void RotateTo(uint16_t digit, bool animate=true); +private: + int16_t ComputeOffset(uint16_t digit); +}; \ No newline at end of file diff --git a/src/c/pebble_bitmap_layer.hpp b/src/c/pebble_bitmap_layer.hpp new file mode 100644 index 0000000..652521c --- /dev/null +++ b/src/c/pebble_bitmap_layer.hpp @@ -0,0 +1,34 @@ +#pragma once + +namespace Pebble { + +class BitmapLayer : public Layer { +public: + BitmapLayer(GRect frame) : Layer((bitmap_layer_ = bitmap_layer_create(frame), bitmap_layer_get_layer(bitmap_layer_))) { } + + BitmapLayer() : BitmapLayer(GRect(0, 0, 0, 0)) {} + + //! Destroys the underlying BitmapLayer + virtual ~BitmapLayer() { + bitmap_layer_destroy(bitmap_layer_); + } + + //! Instances of this class can be automatically converted to Pebble BitmapLayers + inline operator ::BitmapLayer*() { return bitmap_layer_; } + + inline const GBitmap GetBitmap() { return GBitmap(const_cast< ::GBitmap*>(bitmap_layer_get_bitmap(bitmap_layer_))); } + + inline void SetBitmap(GBitmap bitmap) { bitmap_layer_set_bitmap(bitmap_layer_, bitmap); } + + inline void SetBackgroundColor(GColor color) { bitmap_layer_set_background_color(bitmap_layer_, color); } + + inline void SetAlignment(GAlign alignment) { bitmap_layer_set_alignment(bitmap_layer_, alignment); } + + inline void SetCompositingMode(GCompOp mode) { bitmap_layer_set_compositing_mode(bitmap_layer_, mode); } + +protected: + // The layer pointer + ::BitmapLayer *bitmap_layer_; +}; + +} \ No newline at end of file diff --git a/wscript b/wscript new file mode 100644 index 0000000..d2c130e --- /dev/null +++ b/wscript @@ -0,0 +1,67 @@ +# +# This file is the default set of rules to compile a Pebble application. +# +# Feel free to customize this to your needs. +# +import os.path + +top = '.' +out = 'build' + + +def options(ctx): + ctx.load('pebble_sdk') + + +def configure(ctx): + """ + This method is used to configure your build. ctx.load(`pebble_sdk`) automatically configures + a build for each valid platform in `targetPlatforms`. Platform-specific configuration: add your + change after calling ctx.load('pebble_sdk') and make sure to set the correct environment first. + Universal configuration: add your change prior to calling ctx.load('pebble_sdk'). + """ + ctx.load('pebble_sdk') + print(ctx.env.INCLUDES) + ctx.env.CXX = 'arm-none-eabi-g++' + ctx.env.AR = 'arm-none-eabi-ar' + ctx.load('g++') + print(ctx.env.INCLUDES) + + +def build(ctx): + ctx.load('pebble_sdk') + + build_worker = os.path.exists('worker_src') + binaries = [] + + cached_env = ctx.env + for platform in ctx.env.TARGET_PLATFORMS: + ctx.env = ctx.all_envs[platform] + ctx.set_group(ctx.env.PLATFORM_NAME) + + # -- C++ Reconfiguration -- + ctx.env.CXXFLAGS = list(ctx.env.CFLAGS) + ctx.env.CXXFLAGS.remove('-std=c99') + ctx.env.CXXFLAGS.extend(['-std=c++11', '-fPIE', '-fno-unwind-tables', '-fno-exceptions', '-fno-rtti', '-fno-threadsafe-statics']) + # print(ctx.env.CXXFLAGS) + # ------------------------- + + app_elf = '{}/pebble-app.elf'.format(ctx.env.BUILD_DIR) + ctx.pbl_build(source=ctx.path.ant_glob(['src/c/**/*.c','src/c/**/*.cpp']), target=app_elf, bin_type='app') + + if build_worker: + worker_elf = '{}/pebble-worker.elf'.format(ctx.env.BUILD_DIR) + binaries.append({'platform': platform, 'app_elf': app_elf, 'worker_elf': worker_elf}) + ctx.pbl_build(source=ctx.path.ant_glob(['worker_src/c/**/*.c''worker_src/c/**/*.cpp',]), + target=worker_elf, + bin_type='worker') + else: + binaries.append({'platform': platform, 'app_elf': app_elf}) + ctx.env = cached_env + + ctx.set_group('bundle') + ctx.pbl_bundle(binaries=binaries, + js=ctx.path.ant_glob(['src/pkjs/**/*.js', + 'src/pkjs/**/*.json', + 'src/common/**/*.js']), + js_entry_file='src/pkjs/index.js')