From 1ff013964768bf16877d18afc3a3c5f7df68bc1e Mon Sep 17 00:00:00 2001 From: usermaneGermane Date: Fri, 13 Nov 2026 08:38:50 +0200 Subject: [PATCH 1/2] The new look for Linux: light and dark, Manrope, vaults down the left The same as SecretSpace for Windows 3.1.0-alpha.1 and alpha.2: - every colour is a (light, dark) pair; the app follows the desktop and Settings -> Appearance switches; - the Manrope typeface (SIL OFL) travels with the program and is made known through CustomTkinter (copied to ~/.fonts); DejaVu Sans when it cannot be loaded; - the main window has three columns, with the vaults down the left; - Vaults -> Members in the new look; cards without outlines; strength colours readable in both modes. The .deb carries the font and its licence. Its copyright file now says GPL-3.0-only (it still said no licence had been granted) and lists the font's OFL. Version 1.1.0-alpha.2. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_011ojnyVg7ccsR3pZ64s7aRH --- README.md | 9 +- deb/build_deb.py | 15 +- deb/tests/test_build_deb.py | 9 + sourceFiles/Manrope-Bold.ttf | Bin 0 -> 97424 bytes sourceFiles/Manrope-OFL.txt | 93 ++++++++ sourceFiles/Manrope-Regular.ttf | Bin 0 -> 97436 bytes sourceFiles/appconfig.py | 6 + sourceFiles/dialogs.py | 20 +- sourceFiles/gui.py | 371 ++++++++++++++++++++------------ sourceFiles/sharing_ui.py | 47 ++-- sourceFiles/tests/test_look.py | 143 +++++++++++- sourceFiles/theme.py | 146 +++++++++---- sourceFiles/version.py | 2 +- sourceFiles/widgets.py | 41 +++- 14 files changed, 686 insertions(+), 216 deletions(-) create mode 100644 sourceFiles/Manrope-Bold.ttf create mode 100644 sourceFiles/Manrope-OFL.txt create mode 100644 sourceFiles/Manrope-Regular.ttf diff --git a/README.md b/README.md index 845a43f..0bc779b 100644 --- a/README.md +++ b/README.md @@ -213,8 +213,12 @@ with the Secret Key from your Emergency Kit. copied passwords are cleared after 30 seconds — but a clipboard manager (GNOME or KDE clipboard history, CopyQ, …) may keep its own copy, which no application can reach. -- **Fonts.** DejaVu Sans, which Ubuntu ships; elsewhere fontconfig picks the - nearest. +- **Fonts.** From 1.1 the app brings its own typeface, Manrope (SIL Open + Font License); it is copied to `~/.fonts` at the first start, which is + how the toolkit makes a font known. Codes and fingerprints use DejaVu + Sans Mono, which Ubuntu ships. +- **Light and dark** *(1.1)* follow the desktop where it reports one; + *Settings → Appearance* chooses one either way. - **A package instead of an installer.** The `.deb` does what the Windows installer does: a menu entry, data in your home folder, removal that keeps the vaults. Unlike the Windows installer it does not offer to move a vault @@ -249,6 +253,7 @@ Where that README names a Windows version, read the Linux one beside it: | 3.0.0-alpha.7 | 1.0.0-alpha.7 | recovering an account with the 12-word phrase, a new phrase (Server 2.0.0-alpha.4) | | 3.0.0-alpha.8 | 1.0.0-alpha.8 | security fix: signed vault keys (Server 2.0.0-alpha.5) | | 3.0.0 | 1.0.0 | SecretSpace 3, the first release: everything from the alpha rows (Server 2.0.0) | +| 3.1.0-alpha.1, alpha.2 | 1.1.0-alpha.2 | the new look: light and dark, the Manrope typeface, vaults down the left of the main window | (0.2.0 added the `.deb` package, which has no Windows counterpart.) diff --git a/deb/build_deb.py b/deb/build_deb.py index 7672b67..91a45e5 100644 --- a/deb/build_deb.py +++ b/deb/build_deb.py @@ -64,7 +64,8 @@ ] #: Files of sourceFiles/ that go into the package; the tests stay behind. -SOURCE_PATTERNS = (".py", ".png") +#: The typeface (Manrope) travels with the program, licence beside it. +SOURCE_PATTERNS = (".py", ".png", ".ttf", "-OFL.txt") LAUNCHER = f"""#!/bin/sh # -I: the user's PYTHONPATH and ~/.local packages must not replace the @@ -173,8 +174,16 @@ def copyright_text(pins): Files: * Copyright: usermaneGermane -License: none - No licence has been granted yet. +License: GPL-3.0-only + SecretSpace is free software under the GNU General Public License, + version 3 only. On Debian systems its text is in + /usr/share/common-licenses/GPL-3. + +Files: usr/lib/secretspace/sourceFiles/Manrope-*.ttf +Copyright: 2018 The Manrope Project Authors +License: OFL-1.1 + The SIL Open Font License, version 1.1; its text is in + /usr/lib/secretspace/sourceFiles/Manrope-OFL.txt. Files: usr/lib/secretspace/lib/* Comment: bundled Python libraries; each one's licence is in its diff --git a/deb/tests/test_build_deb.py b/deb/tests/test_build_deb.py index f6d1e8a..3f1f422 100644 --- a/deb/tests/test_build_deb.py +++ b/deb/tests/test_build_deb.py @@ -117,6 +117,15 @@ def test_maintainer_scripts_compile_and_clean(tree): assert "py3clean -p secretspace" in read(tree, "DEBIAN", "prerm") +def test_the_typeface_travels_with_its_licence(tree): + src = os.path.join(tree, "usr", "lib", "secretspace", "sourceFiles") + assert {"Manrope-Regular.ttf", "Manrope-Bold.ttf", + "Manrope-OFL.txt"} <= set(os.listdir(src)) + text = read(tree, "usr", "share", "doc", "secretspace", "copyright") + assert "OFL-1.1" in text and "GPL-3.0-only" in text + assert "No licence has been granted" not in text + + def test_copyright_lists_bundled_libraries(tree): assert "fakelib 1.0" in read(tree, "usr", "share", "doc", "secretspace", "copyright") diff --git a/sourceFiles/Manrope-Bold.ttf b/sourceFiles/Manrope-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..674c8659c2fbcf6442636ade4ce938d5ef9ab3ee GIT binary patch literal 97424 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 literal 0 HcmV?d00001 diff --git a/sourceFiles/Manrope-OFL.txt b/sourceFiles/Manrope-OFL.txt new file mode 100644 index 0000000..e271172 --- /dev/null +++ b/sourceFiles/Manrope-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2018 The Manrope Project Authors (https://github.com/googlefonts/manrope) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/sourceFiles/Manrope-Regular.ttf b/sourceFiles/Manrope-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b70c07873336a900beaaadf0a5da460d99b6f767 GIT binary patch literal 97436 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
    4cPvpM|LEF%In1}?(X{<*=0rryS+k#0 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 int: return value if 0 <= value <= 24 * 60 else DEFAULT_AUTOLOCK_MINUTES +def appearance() -> str: + """Light, dark or what the system uses; see theme.APPEARANCES.""" + value = load_config().get("appearance") + return value if value in ("system", "light", "dark") else "system" + + def sort_order() -> str: """How the account list is ordered; see vault.SORT_ORDERS.""" value = load_config().get("sort_order") diff --git a/sourceFiles/dialogs.py b/sourceFiles/dialogs.py index 88dfac8..65e706c 100644 --- a/sourceFiles/dialogs.py +++ b/sourceFiles/dialogs.py @@ -27,7 +27,8 @@ import totp import vault from theme import (F, TEXT, MUTED, GREEN, GREEN_HOVER, INPUT_BG, BORDER, - CARD, CARD_HOVER, ON_ACCENT, MONO, DANGER, tile_colors) + CARD, CARD_HOVER, ON_ACCENT, MONO, DANGER, tile_colors, + apply_appearance) from vault import (add_account, validate_username, export_csv, export_json, update_entry, find_duplicate) from widgets import (Tooltip, dialog_shell, section, setting_row, option_menu, note, @@ -743,6 +744,23 @@ def set_autolock(choice): lock_menu.pack() lock_menu.set(label_for(appconfig.autolock_minutes())) + # ---- look ---- + look = section(wrap, "Appearance") + looks = {"system": "Same as the system", "light": "Light", "dark": "Dark"} + + def set_look(choice): + mode = next(k for k, v in looks.items() if v == choice) + appconfig.save_config(appearance=mode) + apply_appearance(mode) + + look_menu = option_menu( + setting_row(look, "Light or dark", + "Follows your system unless you choose one here.", hint, + first=True), + list(looks.values()), set_look, width=180) + look_menu.pack() + look_menu.set(looks[appconfig.appearance()]) + # ---- sync ---- syn = section(wrap, "Sync") server = app.account.server diff --git a/sourceFiles/gui.py b/sourceFiles/gui.py index 8d52b9a..3332407 100644 --- a/sourceFiles/gui.py +++ b/sourceFiles/gui.py @@ -32,11 +32,13 @@ import vaults_ui import recovery_ui import totp -from theme import (F, BG, HEADER_BG, CARD, CARD_HOVER, SELECTED, INPUT_BG, +from theme import (F, BG, CARD, CARD_HOVER, SELECTED, INPUT_BG, BORDER, TEXT, MUTED, GREEN, GREEN_HOVER, DANGER, - ON_ACCENT, WARN, MONO, + ON_ACCENT, WARN, MONO, DECOR_DISC_A, DECOR_DISC_B, + DECOR_DOT, DECOR_INK, pick, apply_appearance, + strength_color, tile_colors, asset_candidates) -from widgets import (Tooltip, even_width, ghost_button, accent_button, field, +from widgets import (Tooltip, ghost_button, accent_button, field, strength_widget, modal, fit_modal, safe_grab, confirm, STRENGTH_SEGMENTS) from password_generator import estimate_strength @@ -92,7 +94,8 @@ def backdrop_picture(w, h): if w < 2 or h < 2: return None k = BACKDROP_OVERSAMPLE - img = Image.new("RGB", (w * k, h * k), BG) + img = Image.new("RGB", (w * k, h * k), pick(BG)) + green = pick(GREEN) d = ImageDraw.Draw(img) def box(x0, y0, x1, y1): @@ -109,30 +112,30 @@ def stroke(points, fill, width=4): dot(x, y, width / 2, fill) r1, r2 = max(w, h) * 0.17, max(w, h) * 0.21 - dot(w * 0.1, h * 0.16, r1, "#0F2117") - dot(w * 0.93, h * 0.9, r2, "#101A14") + dot(w * 0.1, h * 0.16, r1, pick(DECOR_DISC_A)) + dot(w * 0.93, h * 0.9, r2, pick(DECOR_DISC_B)) # A padlock, top right: a rounded body, a shackle, a keyhole. x, y = w * 0.86, h * 0.2 - d.arc(box(x - 17, y - 32, x + 17, y + 2), start=180, end=360, fill=GREEN, + d.arc(box(x - 17, y - 32, x + 17, y + 2), start=180, end=360, fill=green, width=4 * k) - stroke([(x - 15, y - 15), (x - 15, y - 4)], GREEN) - stroke([(x + 15, y - 15), (x + 15, y - 4)], GREEN) + stroke([(x - 15, y - 15), (x - 15, y - 4)], green) + stroke([(x + 15, y - 15), (x + 15, y - 4)], green) d.rounded_rectangle(box(x - 26, y - 6, x + 26, y + 34), radius=9 * k, - outline=GREEN, width=4 * k) - dot(x, y + 11, 4.5, GREEN) - stroke([(x, y + 13), (x, y + 21)], GREEN) + outline=green, width=4 * k) + dot(x, y + 11, 4.5, green) + stroke([(x, y + 13), (x, y + 21)], green) # A key, bottom left: a ring, a shaft, two teeth. x, y = w * 0.15, h * 0.8 - ink = "#7BE0A6" + ink = pick(DECOR_INK) d.ellipse(box(x - 40, y - 18, x - 4, y + 18), outline=ink, width=4 * k) stroke([(x - 5, y), (x + 44, y)], ink) stroke([(x + 30, y), (x + 30, y + 15)], ink) stroke([(x + 42, y), (x + 42, y + 11)], ink) for fx, fy in ((0.3, 0.1), (0.7, 0.92), (0.96, 0.48), (0.05, 0.55)): - dot(w * fx, h * fy, 4, "#1F3A2A") + dot(w * fx, h * fy, 4, pick(DECOR_DOT)) return img.resize((w, h), Image.LANCZOS) @@ -158,12 +161,91 @@ def _initials(name): return (name[:2] or "?").upper() +class VaultNav: + """ + The vaults down the left side of the main window: one row each, with its + tile, its name and how many entries it has. It answers to the few calls + an option menu takes (``configure(values=)``, ``cget("values")``, + ``get``, ``set``), which is what the rest of the window speaks. + """ + + def __init__(self, parent, command): + self.frame = ctk.CTkFrame(parent, fg_color="transparent") + self.frame.grid_columnconfigure(0, weight=1) + self._command = command + self._values, self._notes, self._current = [], {}, None + self._rows = {} + + def winfo_exists(self): + return self.frame.winfo_exists() + + def cget(self, key): + if key != "values": + raise ValueError(key) + return list(self._values) + + def get(self): + return self._current + + def configure(self, values=None, notes=None): + """``notes``: what stands at the right of each row (a count).""" + values = self._values if values is None else list(values) + notes = self._notes if notes is None else dict(notes) + if values == self._values and notes == self._notes and self._rows: + return + self._values, self._notes = values, notes + for child in self.frame.winfo_children(): + child.destroy() + self._rows = {} + for i, label in enumerate(values): + self._rows[label] = self._row(i, label) + self._paint() + + def set(self, label): + self._current = label + self._paint() + + def _dropdown_callback(self, label): + self.set(label) + self._command(label) + + def _paint(self): + for label, row in self._rows.items(): + row.configure(fg_color=CARD if label == self._current + else "transparent") + + def _row(self, index, label): + row = ctk.CTkFrame(self.frame, fg_color="transparent", + corner_radius=12, height=44) + row.grid(row=index, column=0, sticky="ew", pady=2) + row.grid_columnconfigure(1, weight=1) + if label == ALL_VAULTS: + bg, ink, letter = GREEN, ON_ACCENT, "≡" + else: + bg, ink, _ = tile_colors(label) + letter = label[:1].upper() or "?" + tile = ctk.CTkLabel(row, text=letter, width=28, height=28, + corner_radius=8, fg_color=bg, text_color=ink, + font=F(12, "bold")) + tile.grid(row=0, column=0, padx=(10, 10), pady=8) + name = ctk.CTkLabel(row, text=_clip(label, 16), anchor="w", + font=F(13, "bold"), text_color=TEXT) + name.grid(row=0, column=1, sticky="w") + note = ctk.CTkLabel(row, text=self._notes.get(label, ""), font=F(11), + text_color=MUTED) + note.grid(row=0, column=2, padx=(6, 12)) + for w in (row, tile, name, note): + w.bind("", + lambda e, label=label: self._dropdown_callback(label)) + return row + + class PasswordManagerGUI: def __init__(self, root): self.root = root self.root.title(window_title()) - self.root.geometry("1180x760") - self.root.minsize(980, 640) + self.root.geometry("1320x800") + self.root.minsize(1120, 640) self.root.configure(fg_color=BG) # Every vault of the open account, by id, as account.vault_ids() @@ -590,14 +672,15 @@ def show_login_screen(self): # A drawn backdrop behind the card: shapes only, redrawn to the # window's size. - backdrop = tk.Canvas(self.root, bg=BG, highlightthickness=0, bd=0) + backdrop = tk.Canvas(self.root, bg=pick(BG), highlightthickness=0, + bd=0) backdrop.place(relx=0, rely=0, relwidth=1, relheight=1) backdrop.bind("", lambda e: self._backdrop_resized(backdrop, e.width, e.height)) card = ctk.CTkFrame(backdrop, fg_color=CARD, corner_radius=24, - bg_color=BG, border_width=1, border_color=BORDER) + bg_color=BG) card.place(relx=0.5, rely=0.5, anchor="center") pad = ctk.CTkFrame(card, fg_color="transparent") pad.pack(padx=40, pady=36) @@ -713,27 +796,28 @@ def _draw_backdrop(self, canvas, w, h): @staticmethod def _draw_backdrop_plain(canvas, w, h): + green, ink = pick(GREEN), pick(DECOR_INK) r1, r2 = max(w, h) * 0.17, max(w, h) * 0.21 canvas.create_oval(w * 0.1 - r1, h * 0.16 - r1, w * 0.1 + r1, - h * 0.16 + r1, fill="#0F2117", outline="") + h * 0.16 + r1, fill=pick(DECOR_DISC_A), outline="") canvas.create_oval(w * 0.93 - r2, h * 0.9 - r2, w * 0.93 + r2, - h * 0.9 + r2, fill="#101A14", outline="") + h * 0.9 + r2, fill=pick(DECOR_DISC_B), outline="") # A lock, top right. x, y = w * 0.86, h * 0.2 - canvas.create_rectangle(x - 26, y - 6, x + 26, y + 34, outline=GREEN, + canvas.create_rectangle(x - 26, y - 6, x + 26, y + 34, outline=green, width=4) canvas.create_arc(x - 17, y - 32, x + 17, y + 2, start=0, extent=180, - style="arc", outline=GREEN, width=4) + style="arc", outline=green, width=4) # A key, bottom left. x, y = w * 0.15, h * 0.8 - canvas.create_oval(x - 40, y - 18, x - 4, y + 18, outline="#7BE0A6", + canvas.create_oval(x - 40, y - 18, x - 4, y + 18, outline=ink, width=4) - canvas.create_line(x - 4, y, x + 44, y, fill="#7BE0A6", width=4) - canvas.create_line(x + 30, y, x + 30, y + 16, fill="#7BE0A6", width=4) - canvas.create_line(x + 42, y, x + 42, y + 12, fill="#7BE0A6", width=4) + canvas.create_line(x - 4, y, x + 44, y, fill=ink, width=4) + canvas.create_line(x + 30, y, x + 30, y + 16, fill=ink, width=4) + canvas.create_line(x + 42, y, x + 42, y + 12, fill=ink, width=4) for fx, fy in ((0.3, 0.1), (0.7, 0.92), (0.96, 0.48), (0.05, 0.55)): canvas.create_oval(w * fx - 4, h * fy - 4, w * fx + 4, h * fy + 4, - fill="#1F3A2A", outline="") + fill=pick(DECOR_DOT), outline="") def _draw_login_tiles(self, users): """One tile per account on this PC, plus one to create another.""" @@ -950,142 +1034,130 @@ def go(): # ================================================================== # Main window # ================================================================== + def _nav_button(self, parent, text, command): + """A quiet row of the side bar.""" + return ctk.CTkButton( + parent, text=text, command=command, height=38, corner_radius=12, + fg_color="transparent", hover_color=CARD_HOVER, anchor="w", + text_color=MUTED, font=F(13, "bold")) + def show_main_window(self): self._clear() self.root.grid_columnconfigure(0, weight=1) self.root.grid_rowconfigure(1, weight=1) - # ---- top bar ---- - bar = ctk.CTkFrame(self.root, fg_color=HEADER_BG, corner_radius=0, - height=72) - bar.grid(row=0, column=0, sticky="ew") - bar.grid_propagate(False) - bar.grid_rowconfigure(0, weight=1) - bar.grid_columnconfigure(2, weight=1) - - brand = ctk.CTkFrame(bar, fg_color="transparent") - brand.grid(row=0, column=0, padx=(24, 14)) + # Three columns: the vaults, the entries of the one picked, and the + # selected entry. + shell = ctk.CTkFrame(self.root, fg_color="transparent") + shell.grid(row=1, column=0, sticky="nsew", padx=16, pady=16) + shell.grid_rowconfigure(0, weight=1) + shell.grid_columnconfigure(1, weight=1, minsize=320) + shell.grid_columnconfigure(2, weight=2, minsize=520) + + # ---- side bar ---- + side = ctk.CTkFrame(shell, fg_color="transparent", width=204) + side.grid(row=0, column=0, sticky="ns", padx=(0, 16)) + side.pack_propagate(False) + + brand = ctk.CTkFrame(side, fg_color="transparent") + brand.pack(fill="x", padx=8, pady=(6, 18)) self._logo(brand, small=True).pack(side="left") ctk.CTkLabel(brand, text="SecretSpace", font=F(17, "bold"), text_color=TEXT).pack(side="left", padx=(10, 0)) + # Which vault the list shows. New entries go to the one picked. + self.vault_menu = VaultNav(side, self._on_vault_pick) + self.vault_menu.frame.pack(fill="x") + manage = self._nav_button(side, "Manage vaults…", self.vaults_dialog) + manage.pack(fill="x", pady=(6, 0)) + Tooltip(manage, "Add, rename, share or delete vaults") + + # Bottom up: the account, then the places you go occasionally. + ctk.CTkLabel(side, text=version_string(), font=F(11), + text_color=MUTED).pack(side="bottom", pady=(6, 0)) + me = ctk.CTkFrame(side, fg_color=CARD, corner_radius=14) + me.pack(side="bottom", fill="x") bg, ink, _ = tile_colors(self.current_user) - chip = ctk.CTkFrame(bar, fg_color=INPUT_BG, corner_radius=20, - border_width=1, border_color=BORDER, height=40) - chip.grid(row=0, column=1) - self._circle(chip, _initials(self.current_user), 30, bg, ink, - F(11, "bold"))[0].pack(side="left", padx=(5, 8), pady=5) - ctk.CTkLabel(chip, text=_clip(self.current_user, 24), + who = ctk.CTkFrame(me, fg_color="transparent") + who.pack(fill="x", padx=12, pady=(12, 8)) + self._circle(who, _initials(self.current_user), 34, bg, ink, + F(12, "bold"))[0].pack(side="left") + ctk.CTkLabel(who, text=_clip(self.current_user, 16), font=F(13, "bold"), text_color=TEXT).pack( - side="left", padx=(0, 14)) - Tooltip(chip, "The account you unlocked. Lock to switch to " - "another.") - - self.search_entry = ctk.CTkEntry( - bar, height=44, corner_radius=22, fg_color=INPUT_BG, - border_color=INPUT_BG, font=F(14), - placeholder_text="Search sites and usernames (Ctrl+F)") - self.search_entry.grid(row=0, column=2, sticky="ew", padx=24) - self.search_entry.bind("", self._on_search) - - controls = ctk.CTkFrame(bar, fg_color="transparent") - controls.grid(row=0, column=3, padx=(0, 24)) - syn = self._ghost_button(controls, "Sync", self.sync_now, width=70) - syn.pack(side="left", padx=(0, 8)) + side="left", padx=(10, 0)) + Tooltip(who, "The account you unlocked. Lock to switch to another.") + pair = ctk.CTkFrame(me, fg_color="transparent") + pair.pack(fill="x", padx=12, pady=(0, 12)) + pair.grid_columnconfigure((0, 1), weight=1, uniform="pair") + syn = self._small_button(pair, "Sync", self.sync_now) + syn.grid(row=0, column=0, sticky="ew", padx=(0, 4)) Tooltip(syn, "Sync this account with its server (Settings → Sync)") - imp = self._ghost_button(controls, "Import", self.import_dialog, width=80) - imp.pack(side="left", padx=(0, 8)) - Tooltip(imp, "Import a PassMan 2.x vault, or a CSV or JSON file") - gen = self._ghost_button(controls, "Generate", self.generator_dialog, - width=92) - gen.pack(side="left", padx=(0, 8)) - Tooltip(gen, "Make a strong password") - gear = self._ghost_button(controls, "⚙", self.settings_dialog, width=44) - gear.pack(side="left", padx=(0, 8)) - Tooltip(gear, "Settings") - lock = self._ghost_button(controls, "Lock", self.lock_vault, width=70) - lock.pack(side="left") + lock = self._small_button(pair, "Lock", self.lock_vault) + lock.grid(row=0, column=1, sticky="ew", padx=(4, 0)) Tooltip(lock, "Lock the account and sign out") - # ---- body: the list on the left, the selected entry on the right ---- - body = ctk.CTkFrame(self.root, fg_color="transparent") - body.grid(row=1, column=0, sticky="nsew", padx=24, pady=20) - body.grid_rowconfigure(0, weight=1) - body.grid_columnconfigure(0, weight=2, minsize=340) - body.grid_columnconfigure(1, weight=3, minsize=480) + gear = self._nav_button(side, "Settings", self.settings_dialog) + gear.pack(side="bottom", fill="x", pady=(0, 10)) + imp = self._nav_button(side, "Import", self.import_dialog) + imp.pack(side="bottom", fill="x") + Tooltip(imp, "Import a PassMan 2.x vault, or a CSV or JSON file") + gen = self._nav_button(side, "Password generator", + self.generator_dialog) + gen.pack(side="bottom", fill="x") + Tooltip(gen, "Make a strong password") + # The archive: somewhere you go occasionally; its count tells you + # there is something in it. + self.archive_btn = self._nav_button(side, "Archive", + self.archive_dialog) + self.archive_btn.pack(side="bottom", fill="x") + Tooltip(self.archive_btn, "Entries you removed, kept so they can be " + "restored") - left = ctk.CTkFrame(body, fg_color="transparent") - left.grid(row=0, column=0, sticky="nsew", padx=(0, 20)) + # ---- the entries ---- + left = ctk.CTkFrame(shell, fg_color="transparent") + left.grid(row=0, column=1, sticky="nsew", padx=(0, 16)) left.grid_rowconfigure(2, weight=1) left.grid_columnconfigure(0, weight=1) - # Which vault the list shows, and the way to the Vaults window. - vault_row = ctk.CTkFrame(left, fg_color="transparent") - vault_row.grid(row=0, column=0, sticky="ew", pady=(0, 10)) - vault_row.grid_columnconfigure(0, weight=1) - self.vault_menu = ctk.CTkOptionMenu( - vault_row, height=34, corner_radius=10, values=[ALL_VAULTS], - fg_color=INPUT_BG, button_color=BORDER, - button_hover_color=CARD_HOVER, text_color=TEXT, font=F(12, "bold"), - dropdown_fg_color=CARD, dropdown_hover_color=CARD_HOVER, - dropdown_text_color=TEXT, anchor="w", command=self._on_vault_pick) - self.vault_menu.grid(row=0, column=0, sticky="ew") - Tooltip(self.vault_menu, "Which vault the list shows. New entries go " - "to the vault picked here.") - manage = ctk.CTkButton( - vault_row, text="Vaults", command=self.vaults_dialog, width=76, - height=34, corner_radius=10, fg_color="transparent", - border_width=1, border_color=BORDER, hover_color=CARD_HOVER, - text_color=TEXT, font=F(12, "bold")) - manage.grid(row=0, column=1, padx=(8, 0)) - Tooltip(manage, "Add, rename or delete vaults") - - chips = ctk.CTkFrame(left, fg_color="transparent") - chips.grid(row=1, column=0, sticky="ew", pady=(0, 12)) - self.all_chip = ctk.CTkLabel(chips, text="All", height=34, - corner_radius=17, fg_color=GREEN, - text_color=ON_ACCENT, font=F(12, "bold")) - self.all_chip.pack(side="left") - # The archive sits beside the count: somewhere you go occasionally, - # next to the number that tells you there is something in it. - self.archive_btn = ctk.CTkButton( - chips, text="Archive", command=self.archive_dialog, width=90, - height=34, corner_radius=17, fg_color="transparent", - border_width=1, border_color=BORDER, hover_color=CARD_HOVER, - text_color=TEXT, font=F(12, "bold")) - self.archive_btn.pack(side="left", padx=(8, 0)) - Tooltip(self.archive_btn, "Entries you removed, kept so they can be " - "restored") + top = ctk.CTkFrame(left, fg_color="transparent") + top.grid(row=0, column=0, sticky="ew") + top.grid_columnconfigure(0, weight=1) + self.search_entry = ctk.CTkEntry( + top, height=46, corner_radius=12, fg_color=CARD, + border_color=CARD, font=F(14), + placeholder_text="Search (Ctrl+F)") + self.search_entry.grid(row=0, column=0, sticky="ew") + self.search_entry.bind("", self._on_search) + add = ctk.CTkButton( + top, text="+", command=self.add_account_dialog, width=46, + height=46, corner_radius=12, fg_color=GREEN, + hover_color=GREEN_HOVER, text_color=ON_ACCENT, font=F(22, "bold")) + add.grid(row=0, column=1, padx=(10, 0)) + Tooltip(add, "Add an entry") + + info = ctk.CTkFrame(left, fg_color="transparent") + info.grid(row=1, column=0, sticky="ew", pady=(12, 6)) + self.all_chip = ctk.CTkLabel(info, text="", font=F(12, "bold"), + text_color=MUTED) + self.all_chip.pack(side="left", padx=(6, 0)) sort_menu = ctk.CTkOptionMenu( - chips, width=160, height=34, corner_radius=10, + info, width=160, height=32, corner_radius=10, values=[SORT_LABELS[o] for o in SORT_ORDERS], - fg_color=INPUT_BG, button_color=BORDER, + fg_color=CARD, button_color=CARD, button_hover_color=CARD_HOVER, text_color=TEXT, font=F(12), dropdown_fg_color=CARD, dropdown_hover_color=CARD_HOVER, dropdown_text_color=TEXT, command=self._on_sort) sort_menu.set(SORT_LABELS[self.sort_order]) sort_menu.pack(side="right") - list_card = ctk.CTkFrame(left, fg_color=CARD, corner_radius=20, - border_width=1, border_color=BORDER) - list_card.grid(row=2, column=0, sticky="nsew") - self.list_frame = ctk.CTkScrollableFrame(list_card, fg_color=CARD, + # Rows are cards of their own, straight on the window colour. + self.list_frame = ctk.CTkScrollableFrame(left, fg_color="transparent", corner_radius=0) - self.list_frame.pack(fill="both", expand=True, padx=6, pady=8) + self.list_frame.grid(row=2, column=0, sticky="nsew") self.list_frame.grid_columnconfigure(0, weight=1) - add_row = ctk.CTkFrame(left, fg_color="transparent", height=48) - add_row.grid(row=3, column=0, sticky="ew", pady=(12, 0)) - add = self._accent_button(add_row, "+ Add an entry", - self.add_account_dialog) - add.configure(height=48, corner_radius=16, font=F(14, "bold")) - even_width(add, add_row) - ctk.CTkLabel(left, text=version_string(), font=F(11), - text_color=MUTED).grid(row=4, column=0, pady=(8, 0)) - - self.detail = ctk.CTkFrame(body, fg_color=CARD, corner_radius=24, - border_width=1, border_color=BORDER) - self.detail.grid(row=0, column=1, sticky="nsew") + self.detail = ctk.CTkFrame(shell, fg_color=CARD, corner_radius=20) + self.detail.grid(row=0, column=2, sticky="nsew") self.refresh_account_list() self._start_idle_timer() @@ -1130,9 +1202,17 @@ def _refresh_vault_menu(self): if self.vault_menu is None or not self.vault_menu.winfo_exists(): return names = [self.vault_name(v) for v in self.vaults] + notes, everything = {}, 0 + for vault_id, name in zip(self.vaults, names): + count = len(active_accounts(self.vaults[vault_id])) + everything += count + notes[name] = (f"shared · {count}" + if self.account.is_shared(vault_id) else str(count)) + notes[ALL_VAULTS] = str(everything) # One vault needs no "all of them". self.vault_menu.configure( - values=names if len(names) < 2 else [ALL_VAULTS] + names) + values=names if len(names) < 2 else [ALL_VAULTS] + names, + notes=notes) self.vault_menu.set(self.vault_name(self.vault_filter) if self.vault_filter in self.vaults else (ALL_VAULTS if len(names) > 1 else names[0])) @@ -1168,7 +1248,8 @@ def refresh_account_list(self): if q in a["site"].lower() or q in a["username"].lower()] accounts = sort_accounts(accounts, self.sort_order) - self.all_chip.configure(text=f" All {total} ") + self.all_chip.configure( + text=f"{total} entr{'y' if total == 1 else 'ies'}") self.archive_btn.configure(text=f"Archive {archived}" if archived else "Archive") @@ -1217,7 +1298,7 @@ def _place_rows(self, pending): i, acc = pending.pop(0) cached = self._rows.get(acc["id"]) if cached: - cached[1].grid(row=i, column=0, sticky="ew", pady=2, padx=2) + cached[1].grid(row=i, column=0, sticky="ew", pady=3, padx=2) cached[2] = i else: self._rows[acc["id"]] = [_row_signature(acc, @@ -1253,14 +1334,14 @@ def _add_row(self, i, acc): row = ctk.CTkFrame( self.list_frame, corner_radius=14, fg_color=SELECTED if acc["id"] == self._selected_id else CARD) - row.grid(row=i, column=0, sticky="ew", pady=2, padx=2) + row.grid(row=i, column=0, sticky="ew", pady=3, padx=2) row.grid_columnconfigure(1, weight=1) bg, ink, _ = tile_colors(acc["site"]) tile = ctk.CTkLabel(row, text=(acc["site"][:1].upper() or "?"), width=42, height=42, corner_radius=12, fg_color=bg, text_color=ink, font=F(16, "bold")) - tile.grid(row=0, column=0, padx=(10, 12), pady=9) + tile.grid(row=0, column=0, padx=(12, 12), pady=11) info = ctk.CTkFrame(row, fg_color="transparent") info.grid(row=0, column=1, sticky="w") @@ -1369,11 +1450,13 @@ def _show_detail(self): # In a vault other people are in, a change has a name. if line and acc.get("updated_by") and self.account.is_shared(home): line += " by " + _clip(acc["updated_by"], 24) + # The vault's name on a line of its own: beside the date it ran + # under the buttons. if len(self.vaults) > 1: - line += (" · " if line else "") + _clip(self.vault_name( - self._entry_vault.get(acc["id"])), 24) + line += ("\n" if line else "") + "In " + _clip(self.vault_name( + self._entry_vault.get(acc["id"])), 28) ctk.CTkLabel(names, text=line, font=F(12), text_color=MUTED, - anchor="w").pack(anchor="w") + anchor="w", justify="left").pack(anchor="w") actions = ctk.CTkFrame(band, fg_color="transparent") actions.grid(row=0, column=2, padx=(8, 22)) if writable: @@ -1561,7 +1644,8 @@ def copy_code(): return box def _strength_row(self, parent, password): - name, frac, color = estimate_strength(password) + name, frac, _ = estimate_strength(password) + color = strength_color(name) row = ctk.CTkFrame(parent, fg_color="transparent") row.pack(fill="x", padx=8, pady=(16, 0)) ctk.CTkLabel(row, text="Strength", font=F(12, "bold"), @@ -1684,6 +1768,7 @@ def _on_close(self): if __name__ == "__main__": + apply_appearance(appconfig.appearance()) root = ctk.CTk() app = PasswordManagerGUI(root) root.mainloop() diff --git a/sourceFiles/sharing_ui.py b/sourceFiles/sharing_ui.py index 177f272..f289b9a 100644 --- a/sourceFiles/sharing_ui.py +++ b/sourceFiles/sharing_ui.py @@ -18,15 +18,17 @@ """ import customtkinter as ctk +import pyperclip import account_sync import accounts import changelog from history_ui import local_time from sync_ui import _in_background -from theme import F, TEXT, MUTED, CARD, BORDER, WARN, DANGER, GREEN, MONO +from theme import (F, TEXT, MUTED, CARD, BORDER, WARN, DANGER, GREEN, MONO, + tile_colors) from widgets import (dialog_shell, ghost_button, accent_button, small_button, - field, error_label, note, option_menu, Tooltip) + field, error_label, note, option_menu, Tooltip, avatar) ROLE_LABELS = {accounts.ROLE_OWNER: "Owner", accounts.ROLE_EDITOR: "Editor", accounts.ROLE_READER: "Reader"} @@ -92,13 +94,15 @@ def members_dialog(app, vault_id, changed=None): account = app.account name = app.vault_name(vault_id) owner = account.vault_role(vault_id) == accounts.ROLE_OWNER - top = app._modal(f"Members of “{name}”", 620, 660) + top = app._modal(f"Members of “{name}”", 620, 680) + tile_bg, tile_ink, band = tile_colors(name) shell = dialog_shell( top, f"Members of “{name}”", "Everyone here can open this vault. Owners manage the members; " "editors change entries; readers only read." if owner else f"You are {_a(role_label(account.vault_role(vault_id)).lower())} of " - "this vault. Its owners manage the members.", scroll=True) + "this vault. Its owners manage the members.", scroll=True, + tile=(name[:1].upper() or "?", tile_bg, tile_ink), band=band) listing = shell.body listing.grid_columnconfigure(0, weight=1) state = {"members": [], "busy": False} @@ -117,17 +121,29 @@ def show(members): return for child in listing.winfo_children(): child.destroy() - fp = ctk.CTkFrame(listing, fg_color="transparent") - fp.grid(row=0, column=0, sticky="ew", padx=4, pady=(10, 6)) - ctk.CTkLabel(fp, text="Your fingerprint", font=F(12, "bold"), + fp = ctk.CTkFrame(listing, fg_color=CARD, corner_radius=14) + fp.grid(row=0, column=0, sticky="ew", padx=2, pady=(10, 4)) + fp.grid_columnconfigure(0, weight=1) + words = ctk.CTkFrame(fp, fg_color="transparent") + words.grid(row=0, column=0, sticky="w", padx=(16, 8), pady=(10, 0)) + ctk.CTkLabel(words, text="Your fingerprint", font=F(12), text_color=MUTED).pack(anchor="w") - ctk.CTkLabel(fp, text=account.fingerprint, + ctk.CTkLabel(words, text=account.fingerprint, font=ctk.CTkFont(family=MONO, size=15), text_color=TEXT).pack(anchor="w") + + def copy_fingerprint(): + pyperclip.copy(account.fingerprint) + app.toast("Fingerprint copied") + + small_button(fp, "Copy", copy_fingerprint, width=70).grid( + row=0, column=1, padx=(0, 14), pady=(10, 0)) ctk.CTkLabel(fp, text="Read it to someone who is about to share a " "vault with you, so they know the key is yours.", - font=F(11), text_color=MUTED, wraplength=shell.text_width, - justify="left").pack(anchor="w") + font=F(11), text_color=MUTED, + wraplength=shell.text_width - 40, + justify="left").grid(row=1, column=0, columnspan=2, + sticky="w", padx=16, pady=(2, 10)) for i, member in enumerate(members, start=1): _member_row(listing, i, member) status(f"{len(members)} member{'s' if len(members) != 1 else ''}") @@ -168,12 +184,13 @@ def done(payload): _with_server(app, both, done, failed) def _member_row(parent, index, member): - row = ctk.CTkFrame(parent, fg_color=CARD, corner_radius=14, - border_width=1, border_color=BORDER) + row = ctk.CTkFrame(parent, fg_color=CARD, corner_radius=14) row.grid(row=index, column=0, sticky="ew", padx=2, pady=4) - row.grid_columnconfigure(0, weight=1) + row.grid_columnconfigure(1, weight=1) + avatar(row, member["username"]).grid(row=0, column=0, padx=(14, 0), + pady=12) info = ctk.CTkFrame(row, fg_color="transparent") - info.grid(row=0, column=0, sticky="w", padx=(16, 8), pady=10) + info.grid(row=0, column=1, sticky="w", padx=(12, 8), pady=10) who = member["username"] + (" (you)" if member["me"] else "") ctk.CTkLabel(info, text=who, anchor="w", font=F(14, "bold"), text_color=TEXT).pack(anchor="w") @@ -192,7 +209,7 @@ def _member_row(parent, index, member): if member["me"] or (member["verified"] and not owner): return buttons = ctk.CTkFrame(row, fg_color="transparent") - buttons.grid(row=0, column=1, padx=(0, 14)) + buttons.grid(row=0, column=2, padx=(0, 14)) who = member["username"] if not member["verified"]: check = small_button(buttons, "Check", lambda: check_person(who), diff --git a/sourceFiles/tests/test_look.py b/sourceFiles/tests/test_look.py index 2cfa156..25037fd 100644 --- a/sourceFiles/tests/test_look.py +++ b/sourceFiles/tests/test_look.py @@ -11,10 +11,11 @@ import gui import theme # noqa: F401 (importing it applies the drawing settings) -from theme import BG, GREEN +from theme import BG, GREEN, pick def _rgb(colour): + colour = pick(colour) # the pair's colour for the mode in use return tuple(int(colour[i:i + 2], 16) for i in (1, 3, 5)) @@ -57,3 +58,143 @@ def test_padlock_body_has_round_corners(): x, y = round(w * 0.86), round(h * 0.2) assert _near(picture.getpixel((x - 26, y - 6)), BG) assert _near(picture.getpixel((x, y - 4)), GREEN) # top edge, middle + + +# ---- light and dark --------------------------------------------------------- +PAIRS = ("BG", "HEADER_BG", "CARD", "CARD_HOVER", "SELECTED", "INPUT_BG", + "BORDER", "TEXT", "MUTED", "GREEN", "GREEN_HOVER", "ACCENT_DEEP", + "DANGER", "DANGER_HOVER", "ON_ACCENT", "WARN") + + +@pytest.fixture +def mode(): + """Set an appearance for one test, and put the old one back.""" + import customtkinter as ctk + before = ctk.get_appearance_mode() + yield theme.apply_appearance + ctk.set_appearance_mode(before) + + +@pytest.mark.parametrize("name", PAIRS) +def test_every_colour_is_a_light_and_dark_pair(name): + light, dark = getattr(theme, name) + for colour in (light, dark): + assert len(colour) == 7 and colour.startswith("#") + int(colour[1:], 16) + + +def test_pick_follows_the_mode(mode): + mode("light") + assert pick(theme.BG) == theme.BG[0] and not theme.is_dark() + mode("dark") + assert pick(theme.BG) == theme.BG[1] and theme.is_dark() + assert pick("#123456") == "#123456" # a plain colour stays + + +def test_an_unknown_appearance_means_system(mode): + import customtkinter as ctk + mode("purple") + assert ctk.get_appearance_mode() in ("Light", "Dark") + + +@pytest.mark.parametrize("which", ["light", "dark"]) +def test_backdrop_is_painted_in_the_mode_in_use(mode, which): + mode(which) + picture = gui.backdrop_picture(900, 600) + assert _near(picture.getpixel((450, 300)), theme.BG) + + +def _luminance(colour): + def lin(v): + v /= 255 + return v / 12.92 if v <= 0.03928 else ((v + 0.055) / 1.055) ** 2.4 + r, g, b = (int(colour[i:i + 2], 16) for i in (1, 3, 5)) + return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b) + + +def _contrast(a, b): + hi, lo = sorted((_luminance(a), _luminance(b)), reverse=True) + return (hi + 0.05) / (lo + 0.05) + + +@pytest.mark.parametrize("i", [0, 1]) +@pytest.mark.parametrize("ink,ground", [ + ("TEXT", "BG"), ("TEXT", "CARD"), ("TEXT", "INPUT_BG"), ("MUTED", "BG"), + ("MUTED", "CARD"), ("MUTED", "INPUT_BG"), ("ON_ACCENT", "GREEN"), + ("GREEN", "CARD"), ("WARN", "CARD"), ("TEXT", "SELECTED")]) +def test_text_is_readable_on_its_ground(ink, ground, i): + """WCAG AA for body text: 4.5 to 1, in light (0) and dark (1).""" + assert _contrast(getattr(theme, ink)[i], getattr(theme, ground)[i]) >= 4.5 + + +def test_tile_letters_are_readable(): + for tile, letter, band in theme.TILE_PALETTE: + assert _contrast(tile, letter) >= 4.5 + assert len(band) == 2 + + +def test_the_bundled_font_is_used(): + """The files are in the checkout, so the fallback must not be in use.""" + import os + here = os.path.dirname(os.path.abspath(theme.__file__)) + assert all(os.path.isfile(os.path.join(here, f)) for f in theme.FONT_FILES) + assert theme.FONT == "Manrope" + + +# ---- the vault list at the left of the main window -------------------------- +@pytest.fixture(scope="module") +def window(): + """One Tk window for the module: Tk does not like a second one.""" + import customtkinter as ctk + try: + root = ctk.CTk() + except Exception: # no display (a bare CI runner) + pytest.skip("no display") + root.withdraw() + yield root + root.destroy() + + +@pytest.fixture +def nav(window): + picked = [] + return gui.VaultNav(window, picked.append), picked + + +def test_vault_nav_speaks_like_the_menu_it_replaced(nav): + menu, picked = nav + menu.configure(values=["All vaults", "Personal", "Work"], + notes={"All vaults": "5", "Personal": "3", "Work": "2"}) + menu.set("All vaults") + assert menu.cget("values") == ["All vaults", "Personal", "Work"] + assert menu.get() == "All vaults" and picked == [] # set() only shows + menu._dropdown_callback("Work") # a click + assert menu.get() == "Work" and picked == ["Work"] + assert menu.winfo_exists() + + +def test_vault_nav_marks_the_picked_row_only(nav): + menu, _ = nav + menu.configure(values=["Personal", "Work"], notes={}) + menu.set("Work") + marked = [label for label, row in menu._rows.items() + if row.cget("fg_color") == theme.CARD] + assert marked == ["Work"] + + +def test_vault_nav_keeps_its_rows_when_nothing_changed(nav): + menu, _ = nav + menu.configure(values=["Personal", "Work"], notes={"Work": "2"}) + before = dict(menu._rows) + menu.configure(values=["Personal", "Work"], notes={"Work": "2"}) + assert menu._rows == before # not rebuilt + menu.configure(values=["Personal", "Work"], notes={"Work": "3"}) + assert menu._rows != before # a count changed + + +@pytest.mark.parametrize("name", ["Weak", "Fair", "Good", "Strong"]) +@pytest.mark.parametrize("i", [0, 1]) +def test_strength_colours_show_on_a_card_in_both_modes(name, i): + """A meter and its word, not body text: 3 to 1 is the bar for those.""" + assert _contrast(theme.strength_color(name)[i], theme.CARD[i]) >= 3 + assert theme.strength_color("") == theme.MUTED diff --git a/sourceFiles/theme.py b/sourceFiles/theme.py index 270d315..8cac636 100644 --- a/sourceFiles/theme.py +++ b/sourceFiles/theme.py @@ -3,6 +3,10 @@ Everything visual that more than one module needs lives here, so the colours are defined in exactly one place. + +Every colour is a ``(light, dark)`` pair. CustomTkinter widgets take the pair +as it is and follow the appearance mode by themselves; a plain Tk widget or a +Pillow drawing needs one colour, which :func:`pick` chooses. """ import os @@ -10,7 +14,10 @@ import customtkinter as ctk -ctk.set_appearance_mode("dark") +#: What Settings offers. "system" follows the desktop. +APPEARANCES = ("system", "light", "dark") + +ctk.set_appearance_mode("system") ctk.set_default_color_theme("green") # Rounded corners are drawn the way CustomTkinter prefers here: out of @@ -18,50 +25,57 @@ # which are quicker (about 7 ms per account row, measured on CustomTkinter # 6.0) but not smoothed at all -- every corner showed its pixel steps. -BG = "#0A0C0B" # the window itself -HEADER_BG = "#101311" # top bar, history panel -CARD = "#131715" # cards and panels on top of BG -CARD_HOVER = "#1C231F" -SELECTED = "#18271E" # the selected row in the account list -INPUT_BG = "#1A1F1C" -BORDER = "#27302A" -TEXT = "#E6EDE8" -MUTED = "#8E9A92" -GREEN = "#2BD47D" -GREEN_HOVER = "#22B169" -ACCENT_DEEP = "#177A49" # a selected choice under light text -DANGER = "#E5534B" -DANGER_HOVER = "#C23B34" -ON_ACCENT = "#06110B" # text drawn on top of the green accent -WARN = "#E3B341" - -# Segoe UI and Consolas are Windows fonts. DejaVu ships with Ubuntu and -# most other desktops; where it is missing, fontconfig picks the nearest. -FONT = "DejaVu Sans" -MONO = "DejaVu Sans Mono" +# light dark +BG = ("#EDF0F4", "#15161A") # the window itself +HEADER_BG = ("#E6EAEF", "#1A1C21") # top bar, history panel +CARD = ("#FFFFFF", "#1F2127") # cards and panels on top of BG +CARD_HOVER = ("#E9EDF2", "#2A2D36") +SELECTED = ("#DDF1E6", "#1F3128") # the selected row in the account list +INPUT_BG = ("#F4F6F9", "#191B20") +BORDER = ("#DCE1E8", "#2E313A") +TEXT = ("#16181D", "#F0F1F4") +MUTED = ("#586170", "#A2A8B3") +GREEN = ("#0E7A47", "#56E39F") +GREEN_HOVER = ("#0B6239", "#3FCB87") +ACCENT_DEEP = ("#0E7A47", "#177A49") # a selected choice under light text +DANGER = ("#C0362F", "#E5534B") +DANGER_HOVER = ("#9E2A24", "#C23B34") +ON_ACCENT = ("#FFFFFF", "#05230F") # text drawn on top of the green accent +WARN = ("#8A5300", "#F2B955") -# Per-site tile colours: (tile, letter, header band). Dark tints with a light -# letter, so entries are told apart without breaking the black-and-green look. -TILE_PALETTE = [ - ("#173A27", "#8FF0B9", "#10291B"), - ("#3A2C17", "#F2C27A", "#2A2213"), - ("#2A2340", "#CBB8FF", "#1E1A30"), - ("#3D1E22", "#FFA9B1", "#2D171A"), - ("#172B3D", "#9CCBFF", "#12223A"), - ("#222B35", "#B8C6D6", "#1A222B"), -] +# How strong a password is, by the name estimate_strength gives it. +STRENGTH = { + "Weak": DANGER, + "Fair": WARN, + "Good": ("#1F8A55", "#67C090"), + "Strong": GREEN, +} +# The drawn login backdrop (Pillow / a Tk canvas: use pick()). +DECOR_DISC_A = ("#DCE8E1", "#17211B") +DECOR_DISC_B = ("#E1E6EB", "#1A1D21") +DECOR_DOT = ("#BFD3C7", "#26392E") +DECOR_INK = ("#3F9A6C", "#7BE0A6") -def F(size, weight="normal"): - """Shorthand for a themed font.""" - return ctk.CTkFont(family=FONT, size=size, weight=weight) - +#: The app's typeface, bundled beside the program (SIL Open Font License, +#: Manrope-OFL.txt). FONT falls back when the files are missing. +FONT_FILES = ("Manrope-Regular.ttf", "Manrope-Bold.ttf") +# CustomTkinter makes a font known on Linux by copying it to ~/.fonts, as +# it does with its own. DejaVu ships with Ubuntu and most other desktops; +# where it is missing, fontconfig picks the nearest. +FALLBACK_FONT = "DejaVu Sans" +MONO = "DejaVu Sans Mono" -def tile_colors(s): - """Stable per-site (tile, letter, band) colours.""" - if not s: - return TILE_PALETTE[0] - return TILE_PALETTE[sum(ord(c) for c in s) % len(TILE_PALETTE)] +# Per-site tile colours: (tile, letter, header band). A pastel tile with a +# dark letter in both modes; only the band behind an open entry differs. +TILE_PALETTE = [ + ("#D5F2DD", "#0E4023", ("#E2F6E8", "#1D3527")), + ("#FFE0C2", "#5C2E05", ("#FFEBD8", "#3D2C1B")), + ("#E7DBFF", "#35206B", ("#EFE7FF", "#2B2547")), + ("#FFD9E0", "#641328", ("#FFE6EA", "#3F2228")), + ("#CFE3FF", "#12305E", ("#DCE9FF", "#22324D")), + ("#D6EEF2", "#0C3F49", ("#E3F4F7", "#1C3338")), +] def asset_candidates(name): @@ -75,3 +89,53 @@ def asset_candidates(name): paths.append(name) paths.append(os.path.join("dist", name)) return paths + + +def _load_fonts(): + """Make the bundled typeface known to Tk. True when every file loaded.""" + for name in FONT_FILES: + path = next((p for p in asset_candidates(name) if os.path.isfile(p)), + None) + try: + if path is None or not ctk.FontManager.load_font(path): + return False + except Exception: + return False + return True + + +FONT = "Manrope" if _load_fonts() else FALLBACK_FONT + + +def F(size, weight="normal"): + """Shorthand for a themed font.""" + return ctk.CTkFont(family=FONT, size=size, weight=weight) + + +def is_dark(): + return ctk.get_appearance_mode() == "Dark" + + +def pick(color): + """One colour out of a ``(light, dark)`` pair, for what is not a + CustomTkinter widget. A plain colour is returned as it is.""" + if isinstance(color, (tuple, list)): + return color[1] if is_dark() else color[0] + return color + + +def apply_appearance(mode): + """Switch to "system", "light" or "dark"; anything else means system.""" + ctk.set_appearance_mode(mode if mode in APPEARANCES else "system") + + +def strength_color(name): + """The colour of a strength's name and its meter, in both modes.""" + return STRENGTH.get(name, MUTED) + + +def tile_colors(s): + """Stable per-site (tile, letter, band) colours.""" + if not s: + return TILE_PALETTE[0] + return TILE_PALETTE[sum(ord(c) for c in s) % len(TILE_PALETTE)] diff --git a/sourceFiles/version.py b/sourceFiles/version.py index dc7870a..b422354 100644 --- a/sourceFiles/version.py +++ b/sourceFiles/version.py @@ -12,7 +12,7 @@ import re -__version__ = "1.0.0" +__version__ = "1.1.0-alpha.2" #: The container a 3.0 account's vaults are written in (account_crypto). VAULT_FORMAT = "PMV3" diff --git a/sourceFiles/widgets.py b/sourceFiles/widgets.py index 223a6c9..e16a08a 100644 --- a/sourceFiles/widgets.py +++ b/sourceFiles/widgets.py @@ -15,7 +15,7 @@ import customtkinter as ctk -from theme import (F, FONT, BG, HEADER_BG, CARD, TEXT, MUTED, GREEN, +from theme import (tile_colors, strength_color, F, FONT, BG, HEADER_BG, CARD, TEXT, MUTED, GREEN, GREEN_HOVER, DANGER, DANGER_HOVER, INPUT_BG, BORDER, CARD_HOVER, ON_ACCENT) from password_generator import estimate_strength @@ -47,7 +47,7 @@ def _show(self, _=None): tw.wm_overrideredirect(True) tw.wm_geometry(f"+{x}+{y}") tw.attributes("-topmost", True) - lbl = tk.Label(tw, text=self.text, bg="#0E0E10", fg="#EDEDED", + lbl = tk.Label(tw, text=self.text, bg="#16181D", fg="#F0F1F4", padx=9, pady=4, font=(FONT, 9), bd=0) lbl.pack() @@ -132,7 +132,7 @@ def field_label(parent, text): def entry(parent, show=None, placeholder="", height=44, font=None): return ctk.CTkEntry(parent, height=height, corner_radius=12, - fg_color=INPUT_BG, border_width=1, border_color=BORDER, + fg_color=CARD, border_width=1, border_color=BORDER, show=show, font=font or F(14), placeholder_text=placeholder) @@ -199,7 +199,8 @@ def strength_widget(parent): label.pack(side="left", padx=(6, 0)) def update(password): - name, frac, color = estimate_strength(password) + name, frac, _ = estimate_strength(password) + color = strength_color(name) lit = max(round(frac * STRENGTH_SEGMENTS), 1) if password else 0 for n, seg in enumerate(segments): seg.configure(fg_color=color if n < lit else BORDER) @@ -217,8 +218,24 @@ def _rule(parent): fg_color=BORDER).pack(fill="x") +def avatar(parent, name, size=40): + """A person's initial on a round pastel tile, the colour fixed by the + name.""" + bg, ink, _ = tile_colors(name) + # A frame of a fixed size with the letter placed in it: a label with + # rounded corners grows sideways and the circle becomes an oval. + disc = ctk.CTkFrame(parent, width=size, height=size, + corner_radius=size // 2, fg_color=bg) + disc.pack_propagate(False) + disc.grid_propagate(False) + ctk.CTkLabel(disc, text=(name[:1].upper() or "?"), fg_color="transparent", + text_color=ink, font=F(size * 2 // 5, "bold")).place( + relx=0.5, rely=0.5, anchor="center") + return disc + + def dialog_shell(top, title, subtitle="", tile=None, danger=False, - scroll=False, fit_exact=True): + scroll=False, fit_exact=True, band=None): """ Header band, body and footer bar for a dialog -- the top bar and the details card of the main window, in small. @@ -229,6 +246,8 @@ def dialog_shell(top, title, subtitle="", tile=None, danger=False, buttons stay put. ``fit_exact`` the window takes the height its content needs, so short dialogs lose their empty middle. Scrolling ones keep theirs. + ``band`` the header's colour, for a dialog about one thing that has + a tile (a vault, an entry): the band of its tile colours. Returns a namespace: ``body`` (build into it), ``buttons`` (the footer row), ``heading`` and ``tile`` (to change them live), ``text_width`` @@ -243,7 +262,7 @@ def dialog_shell(top, title, subtitle="", tile=None, danger=False, buttons = ctk.CTkFrame(foot, fg_color="transparent") buttons.pack(fill="x", padx=PAD, pady=14) - head = ctk.CTkFrame(top, fg_color=HEADER_BG, corner_radius=0) + head = ctk.CTkFrame(top, fg_color=band or HEADER_BG, corner_radius=0) head.pack(side="top", fill="x") inner = ctk.CTkFrame(head, fg_color="transparent") inner.pack(fill="x", padx=PAD, pady=(20, 18)) @@ -263,10 +282,12 @@ def dialog_shell(top, title, subtitle="", tile=None, danger=False, justify="left", wraplength=text_width) heading.pack(anchor="w") if subtitle: - ctk.CTkLabel(words, text=subtitle, font=F(12), text_color=MUTED, + ctk.CTkLabel(words, text=subtitle, font=F(12), + text_color=TEXT if band else MUTED, anchor="w", justify="left", wraplength=text_width).pack(anchor="w", pady=(3, 0)) - _rule(head) + if not band: + _rule(head) if scroll: body = ctk.CTkScrollableFrame(top, fg_color="transparent") @@ -285,8 +306,10 @@ def section(parent, title, danger=False, first=False): ctk.CTkLabel(parent, text=title, font=F(12, "bold"), text_color=DANGER if danger else MUTED, anchor="w").pack(anchor="w", pady=(10 if first else 22, 6)) + # A card needs no outline on the window colour; a dangerous one keeps + # its red line. card = ctk.CTkFrame(parent, fg_color=CARD, corner_radius=16, - border_width=1, border_color=DANGER if danger else BORDER) + border_width=1 if danger else 0, border_color=DANGER) card.pack(fill="x") return card From 423cdbeea095ec01c608a56d3735daf055442eeb Mon Sep 17 00:00:00 2001 From: usermaneGermane Date: Fri, 13 Nov 2026 08:43:03 +0200 Subject: [PATCH 2/2] Members: copy the fingerprint with the app's own copy pyperclip is a Windows-only dependency of this app; the Linux client uses Tk's clipboard, and sharing_ui.py is the same file in both. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_011ojnyVg7ccsR3pZ64s7aRH --- sourceFiles/sharing_ui.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sourceFiles/sharing_ui.py b/sourceFiles/sharing_ui.py index f289b9a..e04240e 100644 --- a/sourceFiles/sharing_ui.py +++ b/sourceFiles/sharing_ui.py @@ -18,7 +18,6 @@ """ import customtkinter as ctk -import pyperclip import account_sync import accounts @@ -133,8 +132,8 @@ def show(members): text_color=TEXT).pack(anchor="w") def copy_fingerprint(): - pyperclip.copy(account.fingerprint) - app.toast("Fingerprint copied") + # The app's own copy: the clipboard differs per system. + app._copy(account.fingerprint, "Fingerprint copied") small_button(fp, "Copy", copy_fingerprint, width=70).grid( row=0, column=1, padx=(0, 14), pady=(10, 0))