From 76ade4bf0b713c5318c5e2c5967b504f3bc4cdf5 Mon Sep 17 00:00:00 2001 From: Stu Leak Date: Mon, 3 Nov 2025 11:05:58 -0500 Subject: [PATCH] =?UTF-8?q?v0.1.2=20=E2=80=94=20Header=20finalized=20with?= =?UTF-8?q?=20background=20color=20and=20live=20clock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.json | 18 ++- docs/CHANGELOG.md | 61 +++++++++ docs/README.md | 119 ++++++++++++++++++ main.py | 37 +++++- .../telefact_renderer.cpython-313.pyc | Bin 7354 -> 7611 bytes .../telefact_formatter.cpython-313.pyc | Bin 0 -> 3572 bytes .../telefact_frame.cpython-313.pyc | Bin 3287 -> 3546 bytes .../telefact_header.cpython-313.pyc | Bin 0 -> 6464 bytes src/core/telefact_formatter.py | 76 +++++++++++ src/core/telefact_frame.py | 53 +++++--- src/core/telefact_header.py | 114 +++++++++++++++++ src/telefact_renderer.py | 101 ++++++++++----- 12 files changed, 524 insertions(+), 55 deletions(-) create mode 100644 docs/CHANGELOG.md create mode 100644 docs/README.md create mode 100644 src/core/__pycache__/telefact_formatter.cpython-313.pyc create mode 100644 src/core/__pycache__/telefact_header.cpython-313.pyc diff --git a/config/config.json b/config/config.json index edf17f5..0a2a76c 100644 --- a/config/config.json +++ b/config/config.json @@ -1,16 +1,30 @@ { "Mode": "Broadcaster", + "ScreenWidth": 800, "ScreenHeight": 600, + "ShowGrid": false, + "Font": { "Name": "Modeseven", "Path": "assets/fonts/Modeseven.ttf", "Size": 18 }, - "ShowGrid": false, + "Colours": { "Header": "blue", "Footer": "red", - "Background": "black" + "Background": "black", + "TextPrimary": "white", + "TextAccent": "yellow", + "Clock": "yellow" + }, + + "Header": { + "SelectedPage": "P100", + "CurrentPage": "100", + "ServiceName": "Telefact", + "ServiceFG": "yellow", + "ServiceBG": "red" } } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 0000000..a14546f --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1,61 @@ +# Telefact — Changelog + +Project: Telefact +Repository: Leak Technologies +Language: Python (Tkinter Implementation) +Version Line: v0.1.x (Foundational Builds) +Grid Layout: 40×24 Teletext-Compatible + +------------------------------------------------------------ + +## [v0.1.2] — Header Finalization & Renderer Upgrade (2025-11-03) + +### Added +- Full background rendering support in TelefactRenderer + - Each cell now supports independent background colour + - Renderer draws proper CRT-style safe-area margins +- TelefactHeader finalized + - Centered "Telefact" block with blue background + - White page numbers (P100 / 100) + - Yellow live-updating clock (%b%d %H:%M:%S) + - Accurate Ceefax-style alignment maintained across full grid width +- Config.json updated with Header section for colour and text configuration +- Updated TelefactFrame to support (char, fg, bg) tuples + +### Fixed +- Misaligned header text and spacing between elements +- Incorrect handling of colour blocks for service provider +- Missing current_page display now restored + +### Structure +telefact/ +├── src/ +│ ├── core/ +│ │ ├── telefact_frame.py +│ │ ├── telefact_header.py +│ │ └── telefact_formatter.py +│ ├── telefact_renderer.py +│ └── config_manager.py +├── docs/ +│ ├── README.md +│ └── CHANGELOG.md + +### Next Planned (v0.1.3) +- Introduce footer renderer with subpage counter and colored background band +- Add configuration-driven page rotation logic +- Implement "page tuning" animation for realism (Ceefax-style search effect) + +------------------------------------------------------------ + +## [v0.1.1] — Header Alignment Prototype (2025-11-02) +- Implemented basic header text alignment +- Added real-time clock display without background colour support +- Began refinement of grid-safe text spacing + +------------------------------------------------------------ + +## [v0.1.0] — Initial Python Framework (2025-11-01) +- Core grid model (TelefactFrame) defines 40×24 matrix +- Base TelefactRenderer with safe-area calibration +- Preliminary test layout with header, body, and footer +- Initial config.json support for font, size, and colour diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..8dbb7ac --- /dev/null +++ b/docs/README.md @@ -0,0 +1,119 @@ +# Telefact — Python Edition + +Telefact is a modern re-creation of a Ceefax-style Teletext broadcasting system, written in Python using the Tkinter library. +It emulates the visual and structural behaviour of broadcast Teletext systems, maintaining a strict 40×24 character grid layout. +This version focuses on the "Broadcaster" mode, replicating the look and timing of early digital information services such as BBC Ceefax and Channel 4 Oracle. + +------------------------------------------------------------ +VERSION INFORMATION +------------------------------------------------------------ +Version: v0.1.2 +Language: Python 3.13 +Interface: Tkinter (standard library) +Target: Desktop application (Linux / Windows) +Resolution: 40×24 Teletext grid +Font: Modeseven (Teletext recreation) + +------------------------------------------------------------ +PROJECT GOALS +------------------------------------------------------------ +- Faithfully reproduce the look and feel of classic Teletext pages. +- Maintain accurate grid alignment for all text and graphics. +- Provide a configurable header, footer, and body layout. +- Simulate real broadcast elements such as page numbers and timestamps. +- Establish the foundation for future expansion into dynamic page rotation and live content updates. + +------------------------------------------------------------ +CURRENT FEATURES +------------------------------------------------------------ +- 40×24 Teletext grid rendering via TelefactRenderer. +- Safe-area margins to simulate CRT overscan. +- Dynamic Ceefax-style header displaying: + - Selected page number (e.g., P100) + - Service provider name block (e.g., Telefact) + - Current page number (e.g., 100) + - Real-time clock (updates every second) +- Configurable colours and font settings via config.json. +- Keyboard shortcuts: + - Q or ESC to exit the application. + +------------------------------------------------------------ +SOURCE STRUCTURE +------------------------------------------------------------ +telefact/ +├── src/ +│ ├── core/ +│ │ ├── telefact_frame.py +│ │ ├── telefact_header.py +│ │ └── telefact_formatter.py +│ ├── telefact_renderer.py +│ └── config_manager.py +├── docs/ +│ ├── README.md +│ └── CHANGELOG.md +├── assets/ +│ └── fonts/ +│ ├── Modeseven.ttf +│ ├── EuropeanTeletext.ttf +│ └── EuropeanTeletextNuevo.ttf +└── main.py + +------------------------------------------------------------ +CONFIGURATION OVERVIEW +------------------------------------------------------------ +The configuration file is located at: + config/config.json + +Example: +{ + "Mode": "Broadcaster", + "ScreenWidth": 800, + "ScreenHeight": 600, + "Font": { + "Name": "Modeseven", + "Path": "assets/fonts/Modeseven.ttf", + "Size": 18 + }, + "ShowGrid": false, + "Colours": { + "Header": "blue", + "Footer": "red", + "Background": "black", + "TextPrimary": "white", + "TextAccent": "yellow", + "Clock": "yellow" + }, + "Header": { + "ServiceName": "Telefact", + "ServiceFG": "yellow", + "ServiceBG": "blue", + "SelectedPage": "P100", + "CurrentPage": "100" + } +} + +------------------------------------------------------------ +ROADMAP +------------------------------------------------------------ +v0.1.3 — Footer System and Subpage Counter + - Implement footer band with coloured background and subpage indicator. + - Integrate page tuning animation (Ceefax-style). + - Add configuration options for rotation speed and page timing. + +v0.2.0 — Page System Expansion + - Introduce multiple page definitions with rotation logic. + - Implement Teletext page index and feed loading. + - Begin adding dynamic content from local cache and static feeds. + +v0.3.0 — Broadcaster Automation + - Simulate channel loop broadcasting through scheduled pages. + - Add simple playback manager for testing page timing. + +------------------------------------------------------------ +AUTHOR AND COPYRIGHT +------------------------------------------------------------ +Developed by Stu Leak +Leak Technologies (2025) +All rights reserved. + +Fonts: Modeseven and European Teletext by their respective creators. diff --git a/main.py b/main.py index 060dd01..5c7ce73 100644 --- a/main.py +++ b/main.py @@ -1,33 +1,60 @@ +# ============================================================ +# File: /home/stu/Projects/Local REPO/telefact/main.py +# Description: +# Entry point for the Telefact Broadcaster mode. +# Initializes the Tkinter window, loads configuration, +# and renders the Teletext grid with dynamic header. +# ============================================================ + import tkinter as tk from src.config_manager import ConfigManager from src.telefact_renderer import TelefactRenderer from src.core.telefact_frame import TelefactFrame +from src.core.telefact_formatter import TelefactFormatter +from src.core.telefact_header import TelefactHeader + def main(): + # --- Load configuration --- config = ConfigManager().config + # --- Initialize Tkinter window --- root = tk.Tk() root.title(f"Telefact — Broadcaster ({config['Mode']})") + # --- Create renderer --- renderer = TelefactRenderer( root, width=config["ScreenWidth"], height=config["ScreenHeight"], show_grid=config.get("ShowGrid", False), - font_path=config["Font"]["Path"] + font_path=config["Font"]["Path"], ) + # --- Prepare frame --- frame = TelefactFrame() - for i, ch in enumerate("TELEFACT"): - frame.set_cell(2 + i, 2, ch, "yellow") - for i, ch in enumerate("BROADCASTER BASE"): - frame.set_cell(2 + i, 4, ch, "white") + # --- Initialize header --- + header = TelefactHeader(frame, config) + header.render() + header.update_time(root, renderer) + + # --- Body & footer test content --- + formatter = TelefactFormatter(frame) + formatter.add_body_line(1, "Welcome to the Telefact Broadcaster base.", align="center", color="white") + formatter.add_body_line(3, "Press Q or ESC to exit.", align="center", color="cyan") + formatter.set_footer("PAGE 100 TELEFACT", align="center", color="green") + + # --- Render frame --- renderer.render(frame) + # --- Bind exit keys --- root.bind("", lambda e: root.quit()) root.bind("q", lambda e: root.quit()) + + # --- Start loop --- root.mainloop() + if __name__ == "__main__": main() diff --git a/src/__pycache__/telefact_renderer.cpython-313.pyc b/src/__pycache__/telefact_renderer.cpython-313.pyc index 52a8cfb38fccc3003e379efd947c5a365288015f..4c62ac45ed88e16892a54c7d74fd0ce98d5f3f30 100644 GIT binary patch delta 2186 zcmZWpO>7%Q6rS1juGjzTdL92I?$&i6+tNTsBg<0tW=}g9pE(QO2dE$ENXI!ux)u`-d@hGadE!!8jL!+9 zA`u<#%DG1+A^{h4QFrT-?$N!vPxtGAL5X-K#0l?&_d!m8$`d|4crVyE${;(qm3!lO89L3a6zk`KVzUTrd-a8V^ua+Ai24 zN~GwRG{jgewe+TRl1q5=IEZxLE`At8CN!9?KhNm_`lt8QAy3V<&o%a_?$|OeqeuD8 zsEoDX`JqGgLaCv$VVlO;rdqDonrfY><$`H8)RzjySJY-*9jQ-~szKB-x=b-CX*|{}%4i@cB*qNwuUNQ;YRFDOGEQrZGt02|n(hguKP?oES^kzsEYwkQ6chN0Eg#PM@NO>{uKKA0cMB9-Vo}#BWDR>91${9XC%+a#k z9|7B~sN>zR0eOBrOqEzhDvTktUa2p#KIn$ci#oRh++$ABAUIHm1#~{o(?X<&nzAx!6V$Hh-c6jA=e{kU8+xh};ig$wgw!z{Z>;MzL9_RZN30 z)jMlWf{YEaG@;QX)hWvd*(~{PSxxG0%YK%5h`}=qjxbOGEPU3&=Nb}F^-d$ZSloNE z1oJ$}CTP)II57j{G;}n+0=R%~_~qFHtpjuVyN560&VL;cx+gH}Yx#bZvddDo;}3M7 zd0*StdK!3_TnoqNQ|;8k_|(6W+Umb0B$vJ*|fGk?5&{93ek{zUu4mW@l+cM6kZDqc*%Yw7=7A}(KAZB4gPSBT?NAP>}6XhoN zH(q)_+K=&5^s}D(J=+kf@p(KRqv22_x^rF3)1Zv0Cu<@oIKg_h?mYHnUL0oEA2bym zz7HWEvj@5s@{;t`*cRMJHL-W*j$(IrFQ|svoHV+!UB9{13OoM;FlFPeemBId4O%>0 zErgvc(gy%%oT-#V0LHXXSD<3axsS-R>peta45UdPj{A?i$eauVwo;Z ztD-f9YKgubeVLD5xb(u}$ko2#ws($O;5xE0A8Lm#^NX2JGe5|~s{)E^D?C#AZ7+R2 z@kesw96DdhK9aD7CnkMA`M}I(_64^A-bS{BdJ-LYlIc!5(^0dXL~12GB<#5nlxK~W zVGF?FSjZ9E&6tG3itS;{iZX9t)UL`rNyY45Q^%#*?Uoz=4^aah=S zd1NvB@yw`=fPcQrF<%d_G0z_0grT{Hjle84)|j!S|A)MBA-GVq5tv2ZF`q&i@ID`L z%-6bOzCF6ef@=@?grMCU719fbZ3JeqbQ?v3LfX07i404i1?YFFBlrwW_a9-w;D*%zpt2lp>-4 delta 2009 zcmZWqU2GIp6ux(U_P^6@ciV2av>hwePHhW?mOv{40uftUphE$*aWmbW{h`}kxHFYj zOp3&xHipp64Yqv|h$fZ?qoyw?CX)D|F~+39Xfp;=gPIT@oYE=}ym;;uki^OEcfNDZ zy?4$%=iEEjIzQhgtrQjc5Il9?dNMmo3sPkKHP9}gdZZ$PRIGB{m|#b8ga^rYb6o-v zW#$vQU0rVCcDUH>>GBdUaF^;dhL_HF;PgsZ6c~=Jw|0w64LL8eJvYA1mnVEb}&gZgYY~H~UqRR)z%rw{<3^M^E9G}wSD}3-Z{*7jF zjTlk+0fGL?hw1zH^}T+TYxAk0C^{~XVzp8YJ1nS{4U|B7xHds8cPyo91qr1%C29p9 zY~;bVZ%5~cxqE`E3X)Fvi7Qhl5GcFgjK5Y)ln z{r-!~jZIboO3yIpl;5+~7iz7{31Qq)UD|J(GO|IfgFm}4vhoI$_LzY%y7^H*>$ zt(A5JVqjc}IG#m%TskR|=Ru*%QW*D8S8-$YAgF}RCtCn4A)eNVCC2mFJc*0+Q!eaV z0{aEjkNPkz@F9Ap*c(uBGZ=Fs7q70Nxc_T zYWZz08Kne8d^Nm>Xs1|}Gr3e&ms8oX6X_P3E~y>2gk&b0?NvOa3tmJHFlb<~0l@Od zp(9K^X6mCR;h9-t&;p>i$tK31W6;Ck5Q9Dd3y)fO%y0vh*B-K&nSHSYgb{N#UbR*} zs+5+FLn3tW8v_7m&{80He&6K2DfOM6nOzHk>IuB~h-J^k?%D3S)`fLV3xOtZc>?GC zlm4GPwF{ow#X!+a=u+FqnJbwuQeO^zHuO#Xjc1P9Jo3tS5a;zdiI3l}JiH4bFZ_9W z-6PD`hW^6)>i6h_BPlumicX-toj%t`+0lyY*{p1w(BhE4mfLIvQUjo1as))L60k&~ zgiH1osC&-J;T*hD!Kyj_LU4j{$wqp%waq+apB~ykA6neR`$#oit@jbm! zsYnwir`At578+*~7l&qtXi;>%-;KQW%SiD0Rwep$w0di0&Bf?!^wRFx+PTm|Wplwb z#Vv-)rneThPIu6b#c*W$K;gip-E)IKhTB)%D55O$C|qv`=v&c$qW>KnCsYKB?OxaNC7~A=}NE2bF|vFJnFwj@W+20;r^H$}ITp zBBn_wTrm?ablAmA3xf7{31fpeC~kt#)?e%NGIr)+g$q?ktRfMt=qY7lo^X-F)f$No6dK`fT3(EIZby|M>1!DV)z?J z0c;*)e1{XUup%Ms`x%9AqPm-?;wFM^pyJ4)RKb1^iK8x;c&oTG?uqb3d}{MVda`B( Rc`r<(WwA$<~cT1N& zGP`GtRVn^eftt!#l?C*V-G8`=l>E%kO8rw+suX0|rlV9sZU6Y+5o+ac-Sg76Xm~zqpndOlV15KPym0YvXH?D{4(OZX9g1 zYbnr2J1CR2bWN7fIvQM2Yp!vVT1IO@nl@>zG*wJwkF(~+rRw2&)dVNG_tTrSe8(<8 zh&-QGOus^{f@+wi<$0=KRu?F^<>>gJpL0y&Q7~&D;Vwct48Pr9LidPFr5(=3P80IP z5$e;tFj-hPm&$Wizvox_!Hu() z>3jXx%ckL|lUHs|^!v3G>G!DFZ*i!7?^wrl&k&N>f7DOZ;@!Qj-PyGyewy0q?0tM{{nV3>*K@%%;AXlXr`A&&Z)|4XSrUJf+yy}CfV^Al{SoDha9m`cAY%12lBMV$^ zoMyl>hdVtE)GPFTHWFZ7gqV6PJZIcxVaIZr1c6KobTOCCB*-D?y9%;Gw){5nVC zi9^u7C&rz;b?EMsY71#QzB=}BY<1${#8PZ4)w(ja^7F4#y-VVDJiT)D*O#_3$CpR9 zk}WHxXUU##GF@x$KZ$*Q`AhM$(M@&et2Z`#&p*ptSRVZsO4b%Xe|N+G?A&JW;Iquo z(r8sCE$yqF4?EXJpEaLZlD}!|S-bP1?d0;6rT4cI=?7!WV=oiEn~B~hvEL^*ysxAe ziHqNcN&AoJF*u!B3nGw&V0!SBxBvuCi5g5%SZ`D5!;)g^%{@2(3_|x#{EXLLzf`Zk z906}549;Y82P{%_^HXtOz#6gg@MIW(!1W4-Hz<6p`=ICdq(1?*T@3quvK>vl zjAk~YnYCk&KUx3eOX+!Zgx{J)@k^=$cFabKf=1*1D=;~#B;*B509QIZW%8028VL;# zLrDM-!kR-02$#YMCqkGqux9@mz&5t2A5IPK_bB?bavB7ITaDl;0FMfEFjjfhpq8Pc ze?9f=Kkol_U;w=wrudnpDeTbS@Co6$F@Ox7APs*r1wI)?a59=ePoVGX-cLeq==@`X zj$^$C1Mpu9&V#y#AOi45Nqgt2{!o8%ceAa3N!g009$Z?!v^M)Zes1G~FK#@&@gjZ> zaQZUQy_x7}<7oRzt+j~G9g||su@x3-woAZ(zy7$&)$1*5~0)NY>Z_n6{;|2BBn}>4j z1vlc)2g$_MS%D+L!}A0wMlouv<6vm{`Y{lCEzt3_dY~*TL25mZcJhm5r2u6dZ`^6P z8P9J(Ay#%KEtKFn#sn3T9}7H_7ol927huXO{|NBvBGFtlT$r2y-gq{Rj|>-{v+&4J zbbYQ|s5nR`bp7Ls;nXB4U7xY3=OaYjGKeF(UMQQoPBC;856{BzPGn)*^(neFs6qWm zp-U;oA#g*Rj-LhJHg)*tfZ88be9P1Ir$G>TMWUD^yu&MdUqJUO@;C8%Rg|UEYjgi5 zFjRZU(D|*l?yXGMPO@FPwB6KPm0*OL4O9fFC`osG-En3!b+#JAxkA#d)i_cK(%N1{ zt!fidnqH@X+WB!vy13?52@HSqU*iA@BOeGmVObi1U8;P1^CTa)W69SN@ORStv~F>& z@0vnV7mpji!8p`~D{uiO`-}6oTjb7R9q{nW;lnTI*PenphgaZEe*IDO0pdC+($Nb+ pXfD$0z(Z(x_#Lc<1wq&eMFjD63lX~hPR@QOUlh8&CD3v0{{XYaFev~4 literal 0 HcmV?d00001 diff --git a/src/core/__pycache__/telefact_frame.cpython-313.pyc b/src/core/__pycache__/telefact_frame.cpython-313.pyc index 558292a4b41c8fda32ee227412b3504b412d392a..22ec12c71fde82f4c6aa7e380f73e6e24b3c0850 100644 GIT binary patch delta 1719 zcmZ8iO-vhC5Pom}yk4&{7@HUfusHm90hvT0Dzu4d6jGHoK?+->s6vX2S&S3oMQ@iz zaDh{lL)wB?D6UkgIg(RT_0U_@s+S&efRZ2T_CTxVz|BBVm7Y4YhA3gJ{oc&H_vX#a zH*fvCzSLN?9}Fr49%)0)3Cc=UY%vDfDv6OeF-QY3=s6DO31KA5>3pW5R4{O;?0*5m zDj6q?8ia8MWxNqKxN*VY$Avz@5E{r$;U;BbGifG9ts%mU1g~#gGGuuD<5HhuDBh}1 zDf1mIfUN;EGTDf{wOBILLzjLd*g%+KQ~?Hz5TI&QvtYJHkCuX;n0a$5opC;3=^1k; z@clGpG|ZQ%RuXSd=Nz*nPUO>>TkZ?)=LQv0utA!eoqsqF0$AN9--)IU)(YD)0>IIO^+5OgU%S_Qr^T~`6a@EE{!c&#wwar7oVhok^49xElvN;G4?MNOC7GJG05*eQS|@=s0O zvU7iaW&T-B>)nfgMjI+^{S4cA>s05X{`L6zW#PVm#b0b)QH$5y&^}Mgcph|Un>gFV~LGeVl$+#%Q~Do?4KXf=|cSYsWWNl>5Nrypr%@yndv07 zh*vH=iiHrm(fQG7FZ|u=1vIn)Kr!5tN}RU3gUWnA`0qaMf6B7(CLLu3x5=?)Dv2E( zB)RMU2#vUZD81@M^c+9*To=&ZZ3P)_k{5@^T@@I-`^CHfk*E=!+rf*zgSQMSg8j8UO$Q delta 1442 zcmZWpT}&fY6ux)nPCL_1DbRxD|5^kY$rhF^4;!__EURHx*sLRFK_WKoY`3J_nmZ7~ z6GRi^gC;a1OXGvVH;6v4Px`p5FPfMve`c}qVPkyZ!Gs@)*~F}#GX>)6H2u!GXU_fZ zcfWfx<)()XRY!q>)(ZjBK!(9HhQC zt(bPEIGeOB(Z~+|^G&JBddhK(a_CKozK! zOe$ov#*`+QDNnFCGg%{I_{Z#1?eSLKz~im(xbw3dQ4eDjii}EWQo^d-Lng9i@w1-iY=&_mv;JM+9CefyX9V01Wv*ytWM03fJ$Rjy7Dy z9t3c?7UZA&Z42q8Y%vSTqhO13-doq-vM-ZxWa(n@V#zEfmfJT&of~@RZm>mgn3zJs z(+8#E_~>)ziPqsPd;LluA$i$Z^L7VdZ+Uj3)^)~%&Wy>}Kgym590nzR!pU&F5;J6# zju=b?;a$Zh*RX$kb>>i+iZV?-<Dr8R zZ3m4F)c~g7wApsi0S#cjD<7Fe6+$<_8QGV2Vd=lvXV!&6z%HO-xe&Iwlua*Y_$UKhkYG|`~K@4Tg7uEv{HmhEXF zS|Jlr=PUjE9LF?-fG6vM7ZSSw@DVwXH05ntGWQ9_74z7Q2N6lRP`b2FAS|ccuoiN| zx6lE_eN|Rku>rzz>JJPDK3VB4wPUGw<RL!By_7Y;pCxk8Tt6Yh<3CBO#C-pR>y xAvHN!5%w~w;|M6WtiA-_vzeTg5Cbs81GI5_`!c2UKw^~rskhU_Apvk_{R4)7KcxTw diff --git a/src/core/__pycache__/telefact_header.cpython-313.pyc b/src/core/__pycache__/telefact_header.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d2ed1c3fb342d80d1b5ace6fa7a89d90ee88b9e0 GIT binary patch literal 6464 zcmdT|T}&L;6~42xKeIou%d-5KA2Y!wX8Ff|u`xEb@n4-7+YH+f<9M>n4(qj-U25Cv6M=E={Yky%bG&d zs86}ty}#$4d+t5weD};YW?A@L(h*1}cf!Znr zkop=!eL@K>ik1e;wL)_XXXJ21rq2q2KJ|Ns5Z9?#A{^)A;jv^&v-3M`h#9wG{YvXzT>yO5HbI*E%=Z%4$eS13EkusNH zHgI_GJ=ra2W!k0LhR`H6yOc}}#iA0MXA%ymV|&d47XFa*`{KF5em)MWH&V#bYcuQX zrNFci(=!t67BrST8f(b9DIZp_ntVbeo|A72YFmiil#5+(m|V;fxS2jrw&2EX!ObE# zO)h2$+;XR|1vi)A&ZD-QXSD^jM<~dnMqTJ>3yBH^Zyq)GE!4jC(*U`hCbdumEfbK4 z;D?sm9E*}*agXNcPR5fds%Ult1~j)I(;G2K?m;8sOn=dw+3r)PHCLZ}Q#~e0azfSY zV{$y69L)_MKdo5@<0)D52mp|hDn~Aaqq1g27u4L{DN13M-o@%Y$m#-(4TtHNW*fa4 zQ)SK89ZyQvpP@D0DySQ*i|}=4I_l1K)CoZk#dDyzJ&ldZs^(D)VZ{VW>oQwI(e?UZ zR4Y)jvWO!nj7KuWFj_7ZL=cUo-H7gDc1wC?^#V;^gELWD-%3gVu)QZQoNraL_eiTkrB*3P zPI#C1R>5YGtNE3~jZT1EsK@@)yYNkd=~(yRMbes=Mx0J1O2a)iBvG z{Z87l2BvKBjpn(RVL)y=w@{kPV#zLewdyT_DF7@UBBG*OsW#<%=Gfjjfm@Q`J z9Cm$Xh!GgU0`xieTpttIeOdV6$+;Fn+=M4lYtVM9ygM$3slq1{@iAV#D)VY`gr~{T z7K-v}46oS+<6-G~57?TGh7(a)H#W@*cBW{YBCDbVPDg8jI%uJ<>ysn}C-XcFVzWq; zaE+`5iNIXT-F&9uY@}#|iOL(0-64N3D!s2x-I}~Lb$jym)1qDD$5uSPduJ!kE_-S+ zo|-vku6<5Q@BLNRr(FxDpL$NbV2Qt;!r6gWf{>aOT}rc|Ur^0XWi>?;QvSi`l=cYb zyrMKqo-&R@JOaBKIoTg*2D*g3|GocJr5z9pMquu_orHV`b=n1{Ul1&LiaTK_F2oGv zFg28u5vAHmg6yr%6Er-k@VIS$h$e^mFdvQGkP|!{L25X0h&Rqa;S=&G59p&LDL;}_ zVrndzFw{<4ffp#;DJN3HGKE0XK$v#urn@22*tnbsI&^i@{n(4Tq8Qq0OiZ&#S2Y$` zDrluT@R+vX5+fj-sH)H#sDi2ez42B69r znCBAMkV~-~wsdq560XwCaud|(&0W^UdYI>o8C?B53uw}?S!OK+D}!0@$usJ+ zhEWfAb2N;30wC=`*6n`R{{il5i_?dZL;>un;Q!py!R($6f2Tdc%Kd)|&nZTak zC=2_3ee2Q1&u(V|r^lUt4wOwh=h~M7b>q$zf921lxw@ZT%lPZZ9WU*~Uv_`&X;A~9 ziPv}k*c0!rY3rh^v>CanTFZcnbu`*Fyjrr%F%$R<4=8Wyg-Q3A!+j%;y5V( zAzFMcihyIvCPYX~@Mys3F+vVMjDF1feVfG>z%L))vgG@+^MMz{5Y6DZ31%RN2%F(R zEiq;2-OSHPFIvimWcqaiqZHqUL1^~EMY%Bzl$bqvR~=#^3p_U4^=|0L0CYn#cgT}G zj-3M`_Ij@Py=*1ND751{=or_7^PEB|w4=mjj^n4ou)mlWvbH&Oy> zF(G)*L3st+K^w$)_Mqd|!Cvra8G<|`hR5*E03Q4R;o*uOgux~gnVR@;BEm}u5!q+omO-c3CTR(a0!=B|pV7Z-m>Z@&evWLyVGSvNmiOns+v27*VYZVCWe()f@X{@P|Ir%AiF}Y zFjp*B*oUO=ThOY{vKgAeEWGG8B-0Oh(#LG}=+eiA;Nb@>p1_MD122l48RdZ8TObgA zBQ%1Kq!0>&{qgb*nT~;x!%I{|H-CIjgc})dXZWt>lko9F(KVD7DPIr_yo%w!25}BK zVudZTn{L6<$Mg{sWFQV_@R8P?(j^d4fS{E^FhexXurHa?OVHF!T(Ls!4kW`cxz2ca zcrX$^qBU(W%{+E3o#3J2xN-z41~mmHN&vEd-XUr0a{Yl!{egwR!p$!$AJ>O;rS=3_ z+K-E(8)~-j5a<@ff=G@S!iZQyDRqh7gQQDDuONNJ*1{xkN+V?d(!J%Ld%}IU06alA zX8nSF%I*1@@!0E>R z0R|ZK<9h@?xWIdaOB9EbkyIS(9#MQh6^>^|yrMW1qly}jCFDdBh8?09NlKzf@#r;c zBqpg8HLSy##)8>vEI^k5Y&tmM!D$xtx@PISZX8I7C<3ymF-Z)oDvb@Mz}7_ZQ=;$8 zI5@-{iW%O9%00+_OrBe8Hg2`B%+@$Pyh@;$?|X^Gi@mJvB(v(W+iIqzRRTr2STE)U zy;u-lV&|%_$aWyjt`aEbfdGny9eVNQ9=%w6_q~_c`=Y+W=6c>?wY|;ESw69SY+EH* zUy#1hOQU7TiV_CxnnS&=`#Z&#SOdh%;3cR9UxHfH?Aws2zv$}crtgdsuUQ}Pw5{))7JV~;S*+sq517yA5v0}xT-QUCw| literal 0 HcmV?d00001 diff --git a/src/core/telefact_formatter.py b/src/core/telefact_formatter.py index e69de29..14644cf 100644 --- a/src/core/telefact_formatter.py +++ b/src/core/telefact_formatter.py @@ -0,0 +1,76 @@ +""" +Telefact Formatter +------------------ +Handles layout and text placement for header, body, and footer regions +within a 40×24 Telefact frame. + +This keeps Teletext-style alignment and line boundaries. +""" + +from typing import Literal +from src.core.telefact_frame import TelefactFrame + + +class TelefactFormatter: + """Provides structured access to write header, body, and footer text.""" + + def __init__(self, frame: TelefactFrame): + self.frame = frame + # Define region boundaries (row numbers) + self.header_row = 0 + self.footer_row = frame.rows - 1 + self.body_start = 1 + self.body_end = self.footer_row - 1 + + # ------------------------------------------------------------------ + # text placement utilities + # ------------------------------------------------------------------ + def _place_text( + self, + text: str, + row: int, + align: Literal["left", "center", "right"] = "left", + color: str = "white", + pad_char: str = " ", + ) -> None: + """Place text on a given row with Teletext-style alignment.""" + max_width = self.frame.cols + text = text[:max_width] # clip overflow + + if align == "left": + col_start = 0 + elif align == "center": + col_start = max(0, (max_width - len(text)) // 2) + else: # right + col_start = max(0, max_width - len(text)) + + # pad to maintain clean grid line + padded = text.ljust(max_width, pad_char) + for i, ch in enumerate(padded): + self.frame.set_cell(col_start + i, row, ch, color) + + # ------------------------------------------------------------------ + # header/body/footer helpers + # ------------------------------------------------------------------ + def set_header(self, text: str, align="center", color="yellow") -> None: + """Write the header row (row 0).""" + self._place_text(text, self.header_row, align, color) + + def add_body_line( + self, line_num: int, text: str, align="left", color="white" + ) -> None: + """Write a line in the body area (rows 1–22).""" + target_row = self.body_start + line_num + if self.body_start <= target_row <= self.body_end: + self._place_text(text, target_row, align, color) + + def set_footer(self, text: str, align="center", color="cyan") -> None: + """Write the footer row (row 23).""" + self._place_text(text, self.footer_row, align, color) + + # ------------------------------------------------------------------ + # convenience + # ------------------------------------------------------------------ + def clear(self) -> None: + """Clear the frame (fills with spaces).""" + self.frame.clear() diff --git a/src/core/telefact_frame.py b/src/core/telefact_frame.py index 7821f15..9674e28 100644 --- a/src/core/telefact_frame.py +++ b/src/core/telefact_frame.py @@ -1,42 +1,67 @@ -""" -Telefact Frame (model) -- Defines a 40×24 grid and logical regions. -- No rendering here—pure data model. -""" +# ============================================================ +# File: /home/stu/Projects/Local REPO/telefact/src/core/telefact_frame.py +# Description: +# Telefact Frame (model) +# Defines a 40×24 grid and logical regions for Teletext layout. +# Each cell now supports both foreground and background colours. +# This file contains no rendering logic — pure data model. +# ============================================================ + class TelefactFrame: def __init__(self, cols: int = 40, rows: int = 24): self.cols = cols self.rows = rows + # Define Teletext-like regions self.header_rows = 1 self.footer_rows = 1 self.body_rows = self.rows - (self.header_rows + self.footer_rows) - # grid[row][col] -> (char, color_name) - self.grid = [[(" ", "white") for _ in range(self.cols)] for _ in range(self.rows)] + # grid[row][col] -> (char, fg_color, bg_color) + self.grid = [ + [(" ", "white", "black") for _ in range(self.cols)] + for _ in range(self.rows) + ] - # basic cell ops - def set_cell(self, col: int, row: int, char: str, color: str = "white"): + # -------------------------------------------------------- + # Basic cell operations + # -------------------------------------------------------- + def set_cell( + self, + col: int, + row: int, + char: str, + fg_color: str = "white", + bg_color: str = "black", + ): + """Sets a character and both colours in a given grid cell.""" if 0 <= col < self.cols and 0 <= row < self.rows and char: - self.grid[row][col] = (char[0], color) + self.grid[row][col] = (char[0], fg_color, bg_color) def get_cell(self, col: int, row: int): + """Returns (char, fg, bg) for the given grid position.""" if 0 <= col < self.cols and 0 <= row < self.rows: return self.grid[row][col] - return (" ", "white") + return (" ", "white", "black") - def clear(self, char: str = " ", color: str = "white"): + def clear(self, char: str = " ", fg_color: str = "white", bg_color: str = "black"): + """Clears the entire frame to a given colour and character.""" for r in range(self.rows): for c in range(self.cols): - self.grid[r][c] = (char, color) + self.grid[r][c] = (char, fg_color, bg_color) - # regions + # -------------------------------------------------------- + # Region helpers + # -------------------------------------------------------- def header_region(self): + """Row indices for header region (top row).""" return range(0, self.header_rows) def body_region(self): + """Row indices for main content area.""" return range(self.header_rows, self.header_rows + self.body_rows) def footer_region(self): + """Row indices for footer region (bottom row).""" return range(self.rows - self.footer_rows, self.rows) diff --git a/src/core/telefact_header.py b/src/core/telefact_header.py index e69de29..3b1fa22 100644 --- a/src/core/telefact_header.py +++ b/src/core/telefact_header.py @@ -0,0 +1,114 @@ +# ============================================================ +# File: /home/stu/Projects/Local REPO/telefact/src/core/telefact_header.py +# Description: +# Dynamic Telefact header renderer for broadcaster mode. +# Displays page numbers, centered service name block with +# colored background, and a live timestamp. +# ============================================================ + +import tkinter as tk +from datetime import datetime +from src.core.telefact_frame import TelefactFrame + + +class TelefactHeader: + """ + Final layout (Ceefax style): + ┌────────────────────────────────────────────────────────┐ + │ P100 █ Telefact █ 100 Nov03 12:53:10 │ + └────────────────────────────────────────────────────────┘ + """ + + def __init__(self, frame: TelefactFrame, config: dict): + self.frame = frame + self.config = config + + # --- Read config values --- + colours = config.get("Colours", {}) + header_cfg = config.get("Header", {}) + + # Service name & colours + self.service_name = header_cfg.get("ServiceName", "Telefact") + self.service_fg = header_cfg.get("ServiceFG", colours.get("TextAccent", "yellow")) + self.service_bg = header_cfg.get("ServiceBG", colours.get("Header", "blue")) + + # Page numbers + self.selected_page = header_cfg.get("SelectedPage", "P100") + self.current_page = header_cfg.get("CurrentPage", "100") + + # Colours + self.text_white = colours.get("TextPrimary", "white") + self.time_yellow = colours.get("Clock", "yellow") + + # Layout + self.padding = 1 + self.time_format = "%b%d %H:%M:%S" + + # -------------------------------------------------------- + def _clear_header_row(self): + """Clears only the top row.""" + for c in range(self.frame.cols): + self.frame.set_cell(c, 0, " ", self.text_white, "black") + + def _draw_text(self, col: int, text: str, color: str) -> int: + """Draws text from a given column; returns new cursor position.""" + for i, ch in enumerate(text): + pos = col + i + if pos < self.frame.cols: + self.frame.set_cell(pos, 0, ch, color, "black") + return col + len(text) + + def _draw_service_block(self, text: str, fg: str, bg: str, left_bound: int, right_bound: int): + """Draw centered service block with full blue background.""" + block = f"{' ' * self.padding}{text}{' ' * self.padding}" + total_space = right_bound - left_bound + start_col = left_bound + (total_space // 2) - (len(block) // 2) + start_col = max(start_col, 0) + end_col = start_col + len(block) + + # --- Fill background cells properly --- + for c in range(start_col, end_col): + if 0 <= c < self.frame.cols: + self.frame.set_cell(c, 0, " ", bg, bg) + + # --- Overlay text with proper foreground and background --- + for i, ch in enumerate(text): + pos = start_col + i + self.padding + if 0 <= pos < self.frame.cols: + self.frame.set_cell(pos, 0, ch, fg, bg) + + # -------------------------------------------------------- + def render(self): + """Renders header row with page numbers, service, and clock.""" + self._clear_header_row() + + # Left: selected page + col_left = 0 + col_left = self._draw_text(col_left, self.selected_page, self.text_white) + col_left += 2 # spacing after + + # Right: timestamp (always yellow) + time_text = datetime.now().strftime(self.time_format) + right_start = self.frame.cols - len(time_text) + self._draw_text(right_start, time_text, self.time_yellow) + + # Mid-right: current page number (white) + current_page_len = len(self.current_page) + current_page_start = right_start - (current_page_len + 3) + self._draw_text(current_page_start, self.current_page, self.text_white) + + # Center: service provider (blue block) + self._draw_service_block( + self.service_name, + self.service_fg, + self.service_bg, + left_bound=col_left, + right_bound=current_page_start - 1, + ) + + # -------------------------------------------------------- + def update_time(self, root: tk.Tk, renderer, interval_ms: int = 1000): + """Updates time every second without redrawing other rows.""" + self.render() + renderer.render(self.frame) + root.after(interval_ms, lambda: self.update_time(root, renderer, interval_ms)) diff --git a/src/telefact_renderer.py b/src/telefact_renderer.py index 812069b..5188667 100644 --- a/src/telefact_renderer.py +++ b/src/telefact_renderer.py @@ -1,9 +1,11 @@ -""" -Telefact Renderer (Tkinter) -- Renders a 40×24 Telefact grid within a safe-area margin. -- No header/footer bands; for layout calibration only. -- Pure Python (stdlib only). -""" +# ============================================================ +# File: /home/stu/Projects/Local REPO/telefact/src/telefact_renderer.py +# Description: +# Telefact Renderer (Tkinter) +# Renders a 40×24 Telefact grid with full foreground/background +# colour support, aligned inside a CRT-style safe area margin. +# No header/footer bars — layout calibration only. +# ============================================================ import os import tkinter as tk @@ -11,15 +13,18 @@ from tkinter import font as tkfont from typing import Dict from src.core.telefact_frame import TelefactFrame +# ------------------------------------------------------------ +# Standard Teletext colour palette (8 basic colours) +# ------------------------------------------------------------ PALETTE: Dict[str, str] = { - "black": "#000000", - "blue": "#0000FF", - "red": "#FF0000", + "black": "#000000", + "blue": "#0000FF", + "red": "#FF0000", "magenta": "#FF00FF", - "green": "#00FF00", - "cyan": "#00FFFF", - "yellow": "#FFFF00", - "white": "#FFFFFF", + "green": "#00FF00", + "cyan": "#00FFFF", + "yellow": "#FFFF00", + "white": "#FFFFFF", } @@ -39,27 +44,30 @@ class TelefactRenderer: self.height = height self.show_grid = show_grid - # Safe area for 40×24 grid (simulate CRT overscan) + # Teletext dimensions self.cols, self.rows = 40, 24 + + # Safe area margins (simulate CRT overscan) self.margin_x = 24 self.margin_y = 24 self.inner_w = width - self.margin_x * 2 self.inner_h = height - self.margin_y * 2 + # Per-cell dimensions self.cell_w = self.inner_w // self.cols self.cell_h = self.inner_h // self.rows self.w = self.inner_w self.h = self.inner_h - # Colours + # Base colours self.colors = { "Background": "black", - "Grid": "#303030" + "Grid": "#303030", } if colors: self.colors.update(colors) - # Canvas covers the whole window + # Canvas setup self.canvas = tk.Canvas( root, width=self.width, @@ -72,8 +80,9 @@ class TelefactRenderer: # Font setup self.font = self._load_font(font_path, font_size) - # ---------------------------------------------------------------------- + # ------------------------------------------------------------------ def _load_font(self, font_path: str | None, font_size: int) -> tuple: + """Loads Teletext font or falls back to Courier New.""" if font_path and os.path.exists(font_path): try: font_name = os.path.splitext(os.path.basename(font_path))[0] @@ -91,23 +100,45 @@ class TelefactRenderer: print(f"[warn] Font path not found: {font_path}. Using Courier New.") return ("Courier New", font_size, "bold") - # ---------------------------------------------------------------------- + # ------------------------------------------------------------------ def _gx(self, col: int) -> int: + """Grid X coordinate.""" return self.margin_x + col * self.cell_w def _gy(self, row: int) -> int: + """Grid Y coordinate.""" return self.margin_y + row * self.cell_h - def _draw_char(self, col: int, row: int, char: str, color: str) -> None: - """Draw a single glyph.""" - x = self._gx(col) + 2 - y = self._gy(row) + self.cell_h // 2 - self.canvas.create_text( - x, y, anchor="w", text=char, font=self.font, fill=PALETTE.get(color, color) + # ------------------------------------------------------------------ + def _draw_cell(self, col: int, row: int, char: str, fg: str, bg: str) -> None: + """Draw a full cell (background + glyph).""" + x = self._gx(col) + y = self._gy(row) + + # Draw background block + self.canvas.create_rectangle( + x, + y, + x + self.cell_w, + y + self.cell_h, + fill=PALETTE.get(bg, bg), + outline=PALETTE.get(bg, bg), ) + # Draw text glyph + if char.strip(): + self.canvas.create_text( + x + 2, + y + self.cell_h // 2, + anchor="w", + text=char, + font=self.font, + fill=PALETTE.get(fg, fg), + ) + + # ------------------------------------------------------------------ def _draw_grid(self) -> None: - """Debug overlay for exact cell spacing.""" + """Optional debug overlay for cell alignment.""" for c in range(self.cols + 1): x = self._gx(c) self.canvas.create_line( @@ -119,24 +150,26 @@ class TelefactRenderer: self.margin_x, y, self.margin_x + self.w, y, fill=self.colors["Grid"] ) - # ---------------------------------------------------------------------- + # ------------------------------------------------------------------ def render(self, frame: TelefactFrame) -> None: - """Render just the text grid; no header/footer bars.""" + """Renders the full Telefact grid (foreground + background).""" self.canvas.delete("all") - # Fill total background + # Fill background self.canvas.create_rectangle( - 0, 0, self.width, self.height, + 0, + 0, + self.width, + self.height, fill=PALETTE.get(self.colors["Background"], self.colors["Background"]), width=0, ) - # Draw Teletext cells + # Draw all cells (bg first, then text) for row in range(frame.rows): for col in range(frame.cols): - ch, fg = frame.grid[row][col] - if ch.strip(): - self._draw_char(col, row, ch, fg) + ch, fg, bg = frame.get_cell(col, row) + self._draw_cell(col, row, ch, fg, bg) if self.show_grid: self._draw_grid()