diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..407d359 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,85 @@ +# CONTRIBUTING.md + +First off, thank you for considering contributing to PicoForge! It's contributors like you that help make this project better for everyone. We deeply appreciate your time, effort, and interest in improving the tool. + +This document outlines the guidelines and workflow for contributing to the `picoforge` project. + + + +## Table of Contents + +1. [Code Acquisition (Source Cloning)](#1-code-acquisition-source-cloning) +2. [Basic Workflow](#2-basic-workflow) +3. [Code Style and Formatting](#3-code-style-and-formatting) +4. [Pull Request (PR) Submission Workflow](#4-pull-request-pr-submission-workflow) +5. [Review Process](#5-review-process) +6. [Communication Channels](#6-communication-channels-discuss-before-contributing) +7. [Code of Conduct](#7-code-of-conduct) +8. [Legal Notice](#8-legal-notice) + + + +## 1. Code Acquisition (Source Cloning) + +The primary repository is hosted on GitHub, but a read-only mirror is available. **Mirrors can only be used to clone the source code locally.** + +| Platform | URL | Purpose | +| :---------------------------- | :----------------------------------------------- | :-------------------------------- | +| **GitHub (Primary Source)** | `https://github.com/librekeys/picoforge.git` | Required for Contribution Forking | +| **Personal Mirror** | `https://git.suyogtandel.in/LibreKeys/picoforge` | Read-only viewing and cloning | + + + +## 2. Basic Workflow + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request directly against the `main` branch. + + + +## 3. Code Style and Formatting + +- Follow standard Rust coding guidelines and match the existing code style of the PicoForge source code. +- Always use `cargo fmt` to automatically format your code before committing. +- Write clear, concise commit messages. +- Always remember to update documentation for new features. +- Test on multiple platforms when possible before publishing changes. + + + +## 4. Pull Request (PR) Submission Workflow + +- Before submitting your Pull Request, please ensure you have followed the checklist in our [Pull Request Template](PULL_REQUEST_TEMPLATE.md). +- Ensure that any install or build dependencies are removed before the end of the layer when doing a build, and verify that your code compiles successfully. +- Before proceeding, please review the [general steps for completing a pull request](#2-basic-workflow) in this repository. +- One of the key aspects of validating a pull request is verifying that it doesn't break any existing work. Ensure that your code compiles, passes all tests, and that any CI checks run without errors. +- When opening a Pull Request (PR), please explicitly ask for a review from one of the maintainers. Depending on what is changed in the PR, tag either the repository or package maintainer. +- For more info on the project, please check [README.md](../README.md). + + + +## 5. Review Process + +- Our reviewers and maintainers contribute their free time to this project. Please be patient, as it may take a few days for them to review, approve, or request changes on your PR. We genuinely appreciate your work and want to ensure it integrates well into the project. +- In case your PR goes unanswered for more than 2 weeks, feel free to tag the main maintainer (`@lockedmutex`) in the comments to bring it to their attention. + + + +## 6. Communication Channels (Discuss Before Contributing) + +Matrix is our most preferred option for direct communication, followed by Discord. +- **Matrix**: [Join our Matrix room](https://matrix.to/#/%23librekeys:matrix.org) +- **Discord**: [Join our Discord server](https://discord.gg/6wYBpSHJY2) +- **Discussions**: [GitHub Discussions](https://github.com/librekeys/picoforge/discussions) +- **Issues**: [GitHub Issues](https://github.com/librekeys/picoforge/issues) + +## 7. Code of Conduct + +We are committed to providing a welcoming and inspiring community for all. Please be polite and respectful in all your interactions with the project. + +## 8. Legal Notice + +By contributing code, documentation, or other assets to the PicoForge project, the contributor agrees that the content is 100% authored by them, that they have the necessary rights to the content, and that the content may be provided under the project's existing license. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..ae519bb --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,66 @@ +# Submitting an issue + +Thank you for taking the time to report an issue or suggest a feature for PicoForge! + +Please read our [Contributing Guidelines](CONTRIBUTING.md) to understand how to get involved. If you want to work on an existing issue or a new feature, it is highly recommended to discuss it beforehand to ensure there is no conflicting code or duplicate effort. + + + +## Before submitting an issue, check the following checklist: + +- [ ] I have checked the [Troubleshooting Guide](../docs/Troubleshooting.md) +- [ ] I have searched the existing issues to ensure this has not already been reported +- [ ] I am using the latest version of PicoForge +- [ ] I am using the the latest supported version of picofido/picofido2 + +## The Problem + + + +### What are the steps to reproduce this issue? + + + +1. +2. +3. + +### Expected Results + + + +### Actual Results + + + +### Is there anything else you would like to report? + + + +## Environment + + + +- **OS:** (e.g. Linux, Windows 10, macOS) +- **App Version:** (e.g. 0.1.0, or commit hash if building from source) +- **FIDO Firmware Version:** (if applicable) +- **Device Variant:** (e.g. Raspberry Pi Pico 2, ESP32-S3) + +## Logs + + + +### Console Logs +```text +Paste terminal output here +``` + +### In-App Logs + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..88df039 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,45 @@ +# Pull Request Process + + + +### Checklist + +Ensure that your pull request has followed all the steps below: + +- [ ] Code compiles without errors +- [ ] Code is formatted using `cargo fmt` +- [ ] All tests are passing (skip if no tests exist) +- [ ] Extended the documentation with the changes made in the code (Optional) +- [ ] Added myself to the CREDITS.md file +- [ ] Requested review from one of the maintainers + +### Description + +Please describe the changes in this pull request. What does it do? Why is it needed? + +### Proposed changes + +Why should this pull request be accepted? What problem does it solve? +(If it resolves a previous issue, please link to that issue here.) + +### Types of changes + +Please check the relevant types of changes this PR introduces: + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update +- [ ] Refactoring / Code style change +- [ ] Add packaging for a new platform in the release workflows + +### UI Changes (Optional) + + +- Screenshots / Videos: diff --git a/README.md b/README.md index 7037819..7f5e767 100644 --- a/README.md +++ b/README.md @@ -186,59 +186,61 @@ cargo run ``` picoforge/ -├── Cargo.toml # Rust dependencies and project metadata -├── Cargo.lock # Rust dependency lock file -├── src/ # Source code -│ ├── main.rs # Application entry point -│ ├── logging.rs # Logging infrastructure -│ ├── device/ # Device communication logic -│ │ ├── fido/ # FIDO implementation -│ │ ├── rescue/ # Rescue mode handling -│ │ ├── io.rs # IO Utilities -│ │ ├── error.rs # Device error types -│ │ ├── mod.rs -│ │ └── types.rs # Device data types -│ └── ui/ # GPUI Frontend -│ ├── components/ # Reusable UI components -│ ├── views/ # View definitions -│ ├── assets.rs # Asset loader -│ ├── colors.rs # Color definitions -│ ├── rootview.rs # Root view container -│ ├── ui_types.rs # UI-specific types -│ └── mod.rs -├── data/ # Application data -│ ├── in.suyogtandel.picoforge.desktop -│ └── screenshots/ # Screenshots -├── docs/ # Documentation -├── static/ # Static assets (icons) -├── flake.nix # Nix flake configuration -├── default.nix # Nix development shell -├── shell.nix # Nix development shell -├── picoforge.spec # RPM Spec file -├── package.nix # Nix package definition -├── ci.nix # CI configuration for cachix -├── rustfmt.toml # Rust formatting configuration -├── CREDITS.md # Credits -└── LICENSE # License +├── Cargo.toml # Rust dependencies and project metadata +├── Cargo.lock # Rust dependency lock file +├── Packager.toml # Configuration for cargo-packager +├── src/ # Source code +│ ├── main.rs # Application entry point +│ ├── logging.rs # Logging infrastructure +│ ├── error.rs # Global application error types +│ ├── device/ # Device communication logic +│ │ ├── fido/ # FIDO implementation +│ │ ├── rescue/ # Rescue mode handling +│ │ ├── io.rs # IO Utilities +│ │ ├── mod.rs # Device module declaration +│ │ └── types.rs # Device data types +│ └── ui/ # GPUI Frontend +│ ├── components/ # Reusable UI components +│ ├── views/ # View definitions +│ ├── assets.rs # Asset loader +│ ├── colors.rs # Color definitions +│ ├── rootview.rs # Root view container +│ ├── types.rs # UI-specific types +│ └── mod.rs # UI module declaration +├── data/ # Application data +│ ├── in.suyogtandel.picoforge.desktop # Linux desktop entry file +│ └── screenshots/ # Screenshots for documentation +├── docs/ # Project documentation/wiki files +│ ├── Building.md # Instructions for building from source +│ ├── Home.md # Wiki home page +│ ├── Installation.md # Installation guide +│ └── Troubleshooting.md # Troubleshooting common issues +├── maintainers/ # Scripts and resources for package maintainers +│ └── scripts/ # Utility scripts for automating maintenance tasks +│ ├── update.nix # Nix update script configuration +│ └── update.py # Update script implementation +├── static/ # Static application assets +│ ├── appIcons/ # App icons in various sizes and formats +│ └── icons/ # Internal SVG icons used by the GPUI frontend +├── themes/ # Application themes +│ └── picoforge-zinc.json # Zinc theme configuration file +├── flake.nix # Nix flake configuration +├── flake.lock # Nix flake lock file +├── default.nix # Nix package definition/shell +├── shell.nix # Nix development shell +├── picoforge.spec # RPM Spec file +├── package.nix # Nix package definition +├── ci.nix # CI configuration for cachix +├── rustfmt.toml # Rust formatting configuration +├── CREDITS.md # Credits +└── LICENSE # License ``` ## Contributing -Contributions are welcome (REALLY NEEDED, PLEASE HELP ME)! follow these steps: +Contributions are welcome (REALLY NEEDED, PLEASE HELP US)! -1. Fork the repository -2. Create a feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request - -### Development Guidelines - -- Follow Rust best practices -- Use `cargo fmt` to format the code -- Write clear commit messages -- Update documentation for new features -- Test on multiple platforms when possible +Please check the [CONTRIBUTING.md](.github/CONTRIBUTING.md) file for the full contribution process and development guidelines. ## License @@ -249,6 +251,7 @@ See [LICENSE](LICENSE) for full details. ## Repository Maintainers - **Suyog Tandel** ([@lockedmutex](https://github.com/lockedmutex)) +- **Fabrice Bellamy** ([@Lab-8916100448256](https://github.com/Lab-8916100448256)) - Co-Maintainer ## Package Maintainers diff --git a/src/ui/components/dialog.rs b/src/ui/components/dialog.rs index bca8549..9b912c0 100644 --- a/src/ui/components/dialog.rs +++ b/src/ui/components/dialog.rs @@ -737,3 +737,128 @@ pub fn open_change_pin( .close_button(false) }); } + +pub struct StatusContent { + phase: DialogPhase, + title: SharedString, +} + +impl StatusContent { + pub fn set_success(&mut self, msg: String, cx: &mut Context) { + self.phase = DialogPhase::Success(msg); + cx.notify(); + } + + pub fn set_error(&mut self, msg: String, cx: &mut Context) { + self.phase = DialogPhase::Error(msg); + cx.notify(); + } +} + +impl Render for StatusContent { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let phase = self.phase.clone(); + + match &phase { + DialogPhase::Success(msg) => v_flex() + .gap_4() + .child( + h_flex() + .gap_2() + .items_center() + .child( + gpui_component::Icon::new(gpui_component::IconName::CircleCheck) + .text_color(cx.theme().green) + .with_size(gpui_component::Size::Large), + ) + .child(self.title.clone()), + ) + .child(msg.clone()) + .child( + h_flex().justify_end().child( + Button::new("done") + .primary() + .label("Done") + .on_click(|_, window, cx| { + window.close_dialog(cx); + }), + ), + ) + .into_any_element(), + + DialogPhase::Error(err_msg) => { + v_flex() + .gap_4() + .child( + h_flex() + .gap_2() + .items_center() + .child( + gpui_component::Icon::new(gpui_component::IconName::CircleX) + .text_color(cx.theme().danger) + .with_size(gpui_component::Size::Large), + ) + .child(self.title.clone()), + ) + .child( + div() + .px_3() + .py_2() + .rounded_md() + .bg(cx.theme().danger.opacity(0.1)) + .text_color(cx.theme().danger) + .text_sm() + .child(err_msg.clone()), + ) + .child( + h_flex() + .justify_end() + .child(Button::new("close").label("Close").on_click( + |_, window, cx| { + window.close_dialog(cx); + }, + )), + ) + .into_any_element() + } + + _ => v_flex() + .gap_4() + .items_center() + .child("Applying configuration...") + .child( + Button::new("loading") + .primary() + .label("Applying...") + .loading(true), + ) + .into_any_element(), + } + } +} + +pub fn open_status_dialog( + title: &str, + window: &mut Window, + cx: &mut App, +) -> WeakEntity { + let title_str = SharedString::from(title.to_string()); + let dialog_title = title_str.clone(); + + let content = cx.new(|_cx| StatusContent { + phase: DialogPhase::Loading, + title: title_str, + }); + + let handle = content.downgrade(); + + window.open_dialog(cx, move |dialog, _, _| { + dialog + .title(dialog_title.clone()) + .child(content.clone()) + .overlay_closable(false) + .close_button(false) + }); + + handle +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 4747e88..931ff57 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,9 +1,3 @@ -//! NOTE: Hey this is me lockedmutex, right now the code quality of the entire ui module is shit okay, and this is because -//! AI has been used to convert most of the frontend code from svelte+typescript to GPUI, while it has done most of the conversion -//! due to lack in complete context of code and the way it is structured, it has done a shit job. I did make a lot of changes to -//! the code by myself so it is not complete AI slop. Right now I am trading development time with code quality, I will later -//! improve the code quality and correctly categorize the code into individial components. - pub mod assets; pub mod colors; pub mod components; diff --git a/src/ui/rootview.rs b/src/ui/rootview.rs index c428d2e..ada02ea 100644 --- a/src/ui/rootview.rs +++ b/src/ui/rootview.rs @@ -115,6 +115,7 @@ impl Render for ApplicationRoot { } let dialog_layer = Root::render_dialog_layer(window, cx); + let sheet_layer = Root::render_sheet_layer(window, cx); let title_bar = TitleBar::new().bg(cx.theme().title_bar).child( h_flex() @@ -220,5 +221,6 @@ impl Render for ApplicationRoot { .overflow_hidden() .child(body) .children(dialog_layer) + .children(sheet_layer) } } diff --git a/src/ui/views/about.rs b/src/ui/views/about.rs index 14d3065..967fc11 100644 --- a/src/ui/views/about.rs +++ b/src/ui/views/about.rs @@ -22,7 +22,6 @@ impl AboutView { Card::new().child( v_flex() .items_center() - .justify_center() .gap_4() .py_8() .text_center() @@ -61,12 +60,16 @@ impl AboutView { .child( h_flex() .justify_between() + .items_start() .child("Code By:") .child( - div() + v_flex() .font_medium() .text_color(theme.foreground) - .child("Suyog Tandel, Fabrice Bellamy"), + .items_end() + .child("Suyog Tandel") + .child("Fabrice Bellamy") + .child("Jetcookies"), ), ) .child( diff --git a/src/ui/views/config.rs b/src/ui/views/config.rs index 3ad46df..c12f5df 100644 --- a/src/ui/views/config.rs +++ b/src/ui/views/config.rs @@ -1,6 +1,11 @@ use crate::device::io; use crate::device::types::{AppConfigInput, FullDeviceStatus}; -use crate::ui::components::{card::Card, dialog, dialog::PinPromptContent, page_view::PageView}; +use crate::ui::components::{ + card::Card, + dialog, + dialog::{PinPromptContent, StatusContent}, + page_view::PageView, +}; use crate::ui::types::{LedDriverType, UsbIdentityPreset}; use gpui::*; use gpui_component::button::{ButtonCustomVariant, ButtonVariants}; @@ -50,6 +55,11 @@ impl SelectItem for DriverSelectOption { } } +enum StatusDialogHandle { + Pin(WeakEntity), + Status(WeakEntity), +} + pub struct ConfigView { vendor_select: Entity>>, vid_input: Entity, @@ -211,7 +221,7 @@ impl ConfigView { changes: AppConfigInput, method: crate::device::types::DeviceMethod, pin: Option, - dialog_handle: Option>, + dialog_handle: StatusDialogHandle, cx: &mut Context, ) { self.loading = true; @@ -258,21 +268,38 @@ impl ConfigView { this.device_status = Some(new_status); } - if let Some(ref dh) = dialog_handle { - let _ = dh.update(cx, |d, cx| { - d.set_success( - "Configuration applied successfully.".to_string(), - cx, - ); - }); + match &dialog_handle { + StatusDialogHandle::Pin(dh) => { + let _ = dh.update(cx, |d, cx| { + d.set_success( + "Configuration applied successfully.".to_string(), + cx, + ); + }); + } + StatusDialogHandle::Status(dh) => { + let _ = dh.update(cx, |d, cx| { + d.set_success( + "Configuration applied successfully.".to_string(), + cx, + ); + }); + } } } Err(e) => { log::error!("Error saving config: {}", e); - if let Some(ref dh) = dialog_handle { - let _ = dh.update(cx, |d, cx| { - d.set_error(format!("Failed to apply: {}", e), cx); - }); + match &dialog_handle { + StatusDialogHandle::Pin(dh) => { + let _ = dh.update(cx, |d, cx| { + d.set_error(format!("Failed to apply: {}", e), cx); + }); + } + StatusDialogHandle::Status(dh) => { + let _ = dh.update(cx, |d, cx| { + d.set_error(format!("Failed to apply: {}", e), cx); + }); + } } } } @@ -302,7 +329,7 @@ impl ConfigView { changes.clone(), crate::device::types::DeviceMethod::Fido, Some(pin), - Some(dialog_handle), + StatusDialogHandle::Pin(dialog_handle), cx, ); }); @@ -411,7 +438,14 @@ impl ConfigView { if method == crate::device::types::DeviceMethod::Fido { self.open_pin_dialog(changes, window, cx); } else { - self.write_config_to_device(changes, method, None, None, cx); + let handle = dialog::open_status_dialog("Applying Configuration", window, cx); + self.write_config_to_device( + changes, + method, + None, + StatusDialogHandle::Status(handle), + cx, + ); } } diff --git a/src/ui/views/passkeys.rs b/src/ui/views/passkeys.rs index 8a43f68..5320dd6 100644 --- a/src/ui/views/passkeys.rs +++ b/src/ui/views/passkeys.rs @@ -10,7 +10,7 @@ use crate::ui::components::{ use gpui::*; use gpui_component::button::{Button, ButtonVariant, ButtonVariants}; use gpui_component::{ - ActiveTheme, Icon, Sizable, StyledExt, Theme, WindowExt, + ActiveTheme, Icon, Placement, Sizable, StyledExt, Theme, WindowExt, badge::Badge, h_flex, input::{Input, InputState}, @@ -792,14 +792,25 @@ impl PasskeysView { cx: &mut Context, ) -> impl IntoElement { let cred_clone = cred.clone(); + let cred_for_click = cred.clone(); let delete_listener = cx.listener(move |this, _, window, cx| { this.open_ask_delete_pin(cred_clone.clone(), window, cx); }); + let click_listener = cx.listener(move |this, _, window, cx| { + this.open_credential_details(&cred_for_click, window, cx); + }); + let theme = cx.theme(); div() + .id(SharedString::from(format!( + "cred-card-{}", + cred.credential_id + ))) + .cursor_pointer() + .on_click(click_listener) .border_1() .border_color(theme.border) .rounded_xl() @@ -874,6 +885,141 @@ impl PasskeysView { ) } + fn open_credential_details( + &mut self, + cred: &StoredCredential, + window: &mut Window, + cx: &mut Context, + ) { + let title = if !cred.rp_name.is_empty() { + cred.rp_name.clone() + } else if !cred.rp_id.is_empty() { + cred.rp_id.clone() + } else { + "Passkey Details".to_string() + }; + let rp_id = cred.rp_id.clone(); + let user_name = cred.user_name.clone(); + let display_name = if cred.user_display_name.is_empty() { + "N/A".to_string() + } else { + cred.user_display_name.clone() + }; + let user_id = cred.user_id.clone(); + let credential_id = cred.credential_id.clone(); + + window.open_sheet_at(Placement::Bottom, cx, move |sheet, _, cx| { + let theme = cx.theme(); + + let header_row = h_flex() + .gap_3() + .p_4() + .bg(theme.muted.opacity(0.3)) + .border_1() + .border_color(theme.border) + .rounded_lg() + .child( + div() + .size_12() + .rounded_full() + .bg(theme.primary.opacity(0.1)) + .flex() + .items_center() + .justify_center() + .child( + Icon::default() + .path("icons/key-round.svg") + .text_color(theme.primary) + .size_6(), + ), + ) + .child( + v_flex() + .child(div().font_semibold().child(rp_id.clone())) + .child( + div() + .text_sm() + .text_color(theme.muted_foreground) + .font_family("monospace") + .child(user_name.clone()), + ), + ); + + let separator = div().w_full().h(px(1.)).bg(theme.border); + + let detail_field = |label: &str, value: String, mono: bool| { + let mut value_el = div().text_sm().font_medium().child(value.clone()); + if mono { + value_el = div() + .text_xs() + .font_family("monospace") + .bg(theme.muted) + .p_2() + .rounded_md() + .overflow_hidden() + .child(value); + } + v_flex() + .gap_1() + .child( + div() + .text_sm() + .font_medium() + .text_color(theme.muted_foreground) + .child(label.to_string()), + ) + .child(value_el) + }; + + let description = h_flex() + .gap_1() + .child( + div() + .text_sm() + .text_color(theme.muted_foreground) + .child("Credential details for user"), + ) + .child( + div() + .text_sm() + .font_semibold() + .text_color(theme.foreground) + .child(user_name.clone()), + ); + + sheet + .title( + div().w_full().child( + v_flex() + .mx_auto() + .max_w(px(512.)) + .px_4() + .gap_0p5() + .child(div().text_2xl().font_bold().child(title.clone())) + .child(description), + ), + ) + .size(px(500.)) + .resizable(false) + .margin_top(px(0.)) + .child( + div().mx_auto().max_w(px(512.)).w_full().px_4().child( + v_flex() + .gap_4() + .child(header_row) + .child(separator) + .child(detail_field("Display Name", display_name.clone(), false)) + .child(detail_field("User ID (Hex)", user_id.clone(), true)) + .child(detail_field( + "Credential ID (Hex)", + credential_id.clone(), + true, + )), + ), + ) + }); + } + fn open_ask_delete_pin( &mut self, cred: StoredCredential, diff --git a/themes/picoforge-zinc.json b/themes/picoforge-zinc.json index c8c2a2f..65bb24c 100644 --- a/themes/picoforge-zinc.json +++ b/themes/picoforge-zinc.json @@ -7,8 +7,8 @@ { "name": "PicoForge Zinc Light", "mode": "light", - "radius": 10, - "radius.lg": 12, + "radius": 8, + "radius.lg": 10, "colors": { "accent.background": "#f4f4f5", "accent.foreground": "#27272a", @@ -117,8 +117,8 @@ { "name": "PicoForge Zinc Dark", "mode": "dark", - "radius": 10, - "radius.lg": 12, + "radius": 8, + "radius.lg": 10, "colors": { "accent.background": "#27272a", "accent.foreground": "#fafafa",