initial commit
This commit is contained in:
178
.gitignore
vendored
Normal file
178
.gitignore
vendored
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,rust,swift
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,rust,swift
|
||||||
|
|
||||||
|
### Linux ###
|
||||||
|
*~
|
||||||
|
|
||||||
|
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||||
|
.fuse_hidden*
|
||||||
|
|
||||||
|
# KDE directory preferences
|
||||||
|
.directory
|
||||||
|
|
||||||
|
# Linux trash folder which might appear on any partition or disk
|
||||||
|
.Trash-*
|
||||||
|
|
||||||
|
# .nfs files are created when an open file is removed but is still being accessed
|
||||||
|
.nfs*
|
||||||
|
|
||||||
|
### macOS ###
|
||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
### macOS Patch ###
|
||||||
|
# iCloud generated files
|
||||||
|
*.icloud
|
||||||
|
|
||||||
|
### Rust ###
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
### Swift ###
|
||||||
|
# Xcode
|
||||||
|
#
|
||||||
|
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||||
|
|
||||||
|
## User settings
|
||||||
|
xcuserdata/
|
||||||
|
|
||||||
|
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
|
||||||
|
*.xcscmblueprint
|
||||||
|
*.xccheckout
|
||||||
|
|
||||||
|
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
|
||||||
|
build/
|
||||||
|
DerivedData/
|
||||||
|
*.moved-aside
|
||||||
|
*.pbxuser
|
||||||
|
!default.pbxuser
|
||||||
|
*.mode1v3
|
||||||
|
!default.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
!default.mode2v3
|
||||||
|
*.perspectivev3
|
||||||
|
!default.perspectivev3
|
||||||
|
|
||||||
|
## Obj-C/Swift specific
|
||||||
|
*.hmap
|
||||||
|
|
||||||
|
## App packaging
|
||||||
|
*.ipa
|
||||||
|
*.dSYM.zip
|
||||||
|
*.dSYM
|
||||||
|
|
||||||
|
## Playgrounds
|
||||||
|
timeline.xctimeline
|
||||||
|
playground.xcworkspace
|
||||||
|
|
||||||
|
# Swift Package Manager
|
||||||
|
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||||
|
# Packages/
|
||||||
|
# Package.pins
|
||||||
|
# Package.resolved
|
||||||
|
# *.xcodeproj
|
||||||
|
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
|
||||||
|
# hence it is not needed unless you have added a package configuration file to your project
|
||||||
|
# .swiftpm
|
||||||
|
|
||||||
|
.build/
|
||||||
|
|
||||||
|
# CocoaPods
|
||||||
|
# We recommend against adding the Pods directory to your .gitignore. However
|
||||||
|
# you should judge for yourself, the pros and cons are mentioned at:
|
||||||
|
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||||
|
# Pods/
|
||||||
|
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||||
|
# *.xcworkspace
|
||||||
|
|
||||||
|
# Carthage
|
||||||
|
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||||
|
# Carthage/Checkouts
|
||||||
|
|
||||||
|
Carthage/Build/
|
||||||
|
|
||||||
|
# Accio dependency management
|
||||||
|
Dependencies/
|
||||||
|
.accio/
|
||||||
|
|
||||||
|
# fastlane
|
||||||
|
# It is recommended to not store the screenshots in the git repo.
|
||||||
|
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
|
||||||
|
# For more information about the recommended setup visit:
|
||||||
|
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||||
|
|
||||||
|
fastlane/report.xml
|
||||||
|
fastlane/Preview.html
|
||||||
|
fastlane/screenshots/**/*.png
|
||||||
|
fastlane/test_output
|
||||||
|
|
||||||
|
# Code Injection
|
||||||
|
# After new code Injection tools there's a generated folder /iOSInjectionProject
|
||||||
|
# https://github.com/johnno1962/injectionforxcode
|
||||||
|
|
||||||
|
iOSInjectionProject/
|
||||||
|
|
||||||
|
### Windows ###
|
||||||
|
# Windows thumbnail cache files
|
||||||
|
Thumbs.db
|
||||||
|
Thumbs.db:encryptable
|
||||||
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
|
|
||||||
|
# Dump file
|
||||||
|
*.stackdump
|
||||||
|
|
||||||
|
# Folder config file
|
||||||
|
[Dd]esktop.ini
|
||||||
|
|
||||||
|
# Recycle Bin used on file shares
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
|
# Windows Installer files
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
|
# Windows shortcuts
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,rust,swift
|
||||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/runa.iml" filepath="$PROJECT_DIR$/.idea/runa.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
11
.idea/runa.iml
generated
Normal file
11
.idea/runa.iml
generated
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="EMPTY_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "novem"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
gpui = { version = "*" }
|
||||||
|
paste = "1.0"
|
||||||
122
build.rs
Normal file
122
build.rs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct AssetFile {
|
||||||
|
virtual_path: String,
|
||||||
|
disk_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let manifest_dir =
|
||||||
|
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("missing CARGO_MANIFEST_DIR"));
|
||||||
|
let asset_root = manifest_dir.join("src/asset");
|
||||||
|
let out_file = PathBuf::from(env::var("OUT_DIR").expect("missing OUT_DIR")).join("asset.rs");
|
||||||
|
|
||||||
|
println!("cargo::rerun-if-changed={}", asset_root.display());
|
||||||
|
|
||||||
|
let mut directory_entries = BTreeMap::<String, BTreeSet<String>>::new();
|
||||||
|
directory_entries
|
||||||
|
.entry(String::new())
|
||||||
|
.or_default()
|
||||||
|
.insert(String::from("asset"));
|
||||||
|
|
||||||
|
let mut asset_files = Vec::new();
|
||||||
|
collect_assets(
|
||||||
|
&asset_root,
|
||||||
|
"asset",
|
||||||
|
&mut directory_entries,
|
||||||
|
&mut asset_files,
|
||||||
|
);
|
||||||
|
|
||||||
|
let generated = render_assets(&asset_files, &directory_entries);
|
||||||
|
|
||||||
|
fs::write(out_file, generated).expect("failed to write generated assets module");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_assets(
|
||||||
|
disk_dir: &Path,
|
||||||
|
virtual_dir: &str,
|
||||||
|
directory_entries: &mut BTreeMap<String, BTreeSet<String>>,
|
||||||
|
asset_files: &mut Vec<AssetFile>,
|
||||||
|
) {
|
||||||
|
let mut entries = fs::read_dir(disk_dir)
|
||||||
|
.unwrap_or_else(|err| panic!("failed to read {}: {err}", disk_dir.display()))
|
||||||
|
.map(|entry| entry.expect("failed to read directory entry"))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
entries.sort_by_key(|entry| entry.file_name());
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
let file_name = entry
|
||||||
|
.file_name()
|
||||||
|
.into_string()
|
||||||
|
.unwrap_or_else(|_| panic!("non-utf8 asset name in {}", disk_dir.display()));
|
||||||
|
let child_virtual_path = format!("{virtual_dir}/{file_name}");
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
directory_entries
|
||||||
|
.entry(String::from(virtual_dir))
|
||||||
|
.or_default()
|
||||||
|
.insert(file_name);
|
||||||
|
|
||||||
|
if path.is_dir() {
|
||||||
|
collect_assets(&path, &child_virtual_path, directory_entries, asset_files);
|
||||||
|
} else if path.is_file() {
|
||||||
|
asset_files.push(AssetFile {
|
||||||
|
virtual_path: child_virtual_path,
|
||||||
|
disk_path: path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_assets(
|
||||||
|
asset_files: &[AssetFile],
|
||||||
|
directory_entries: &BTreeMap<String, BTreeSet<String>>,
|
||||||
|
) -> String {
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
|
output.push_str(
|
||||||
|
"pub fn load_asset(path: &str) -> gpui::Result<Option<std::borrow::Cow<'static, [u8]>>> {\n",
|
||||||
|
);
|
||||||
|
output.push_str(" match path {\n");
|
||||||
|
for file in asset_files {
|
||||||
|
output.push_str(" ");
|
||||||
|
output.push_str(&string_literal(&file.virtual_path));
|
||||||
|
output.push_str(" => Ok(Some(std::borrow::Cow::Borrowed(include_bytes!(");
|
||||||
|
output.push_str(&string_literal(&file.disk_path.to_string_lossy()));
|
||||||
|
output.push_str(")))),\n");
|
||||||
|
}
|
||||||
|
output.push_str(" _ => Err(anyhow::anyhow!(\"asset not found: {path}\")),\n");
|
||||||
|
output.push_str(" }\n");
|
||||||
|
output.push_str("}\n\n");
|
||||||
|
|
||||||
|
output.push_str("pub fn list_assets(path: &str) -> gpui::Result<Vec<gpui::SharedString>> {\n");
|
||||||
|
output.push_str(" let normalized = path.trim_end_matches('/');\n");
|
||||||
|
output.push_str(" let normalized = if normalized == \".\" { \"\" } else { normalized };\n");
|
||||||
|
output.push_str(" let entries: &[&str] = match normalized {\n");
|
||||||
|
for (directory, entries) in directory_entries {
|
||||||
|
output.push_str(" ");
|
||||||
|
output.push_str(&string_literal(directory));
|
||||||
|
output.push_str(" => &[\n");
|
||||||
|
for entry in entries {
|
||||||
|
output.push_str(" ");
|
||||||
|
output.push_str(&string_literal(entry));
|
||||||
|
output.push_str(",\n");
|
||||||
|
}
|
||||||
|
output.push_str(" ],\n");
|
||||||
|
}
|
||||||
|
output.push_str(" _ => &[],\n");
|
||||||
|
output.push_str(" };\n");
|
||||||
|
output.push_str(" Ok(entries.iter().copied().map(gpui::SharedString::from).collect())\n");
|
||||||
|
output.push_str("}\n");
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_literal(value: &str) -> String {
|
||||||
|
format!("{value:?}")
|
||||||
|
}
|
||||||
1
src/api.rs
Normal file
1
src/api.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub(crate) mod repo;
|
||||||
17
src/api/repo.rs
Normal file
17
src/api/repo.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use crate::query;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct List;
|
||||||
|
|
||||||
|
impl query::QueryFn for List {
|
||||||
|
type Data = ();
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn key(&self) -> &'static str {
|
||||||
|
"repo.list"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self) -> Result<Self::Data, Self::Error> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/app.rs
Normal file
58
src/app.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
use gpui::{div, prelude::*};
|
||||||
|
|
||||||
|
use crate::app;
|
||||||
|
use crate::query;
|
||||||
|
use crate::dashboard;
|
||||||
|
use crate::theme;
|
||||||
|
use crate::titlebar;
|
||||||
|
|
||||||
|
pub struct Global {
|
||||||
|
pub safe_area: gpui::Bounds<gpui::Pixels>,
|
||||||
|
pub current_theme: theme::Theme,
|
||||||
|
pub query_store: query::Store
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Chrome {}
|
||||||
|
|
||||||
|
impl Chrome {
|
||||||
|
pub fn new(window: &mut gpui::Window, cx: &mut gpui::Context<Self>) -> Self {
|
||||||
|
cx.observe_window_appearance(window, |_, window, cx| {
|
||||||
|
cx.update_global::<app::Global, ()>(|global, _cx| {
|
||||||
|
global.current_theme = window.appearance().into();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::Render for Chrome {
|
||||||
|
fn render(
|
||||||
|
&mut self,
|
||||||
|
_window: &mut gpui::Window,
|
||||||
|
cx: &mut gpui::Context<Self>,
|
||||||
|
) -> impl gpui::IntoElement {
|
||||||
|
let title_bar = cx.new(|_| titlebar::TitleBar {});
|
||||||
|
|
||||||
|
let dashboard = cx.new(|_| dashboard::Screen {
|
||||||
|
text: "World".into(),
|
||||||
|
});
|
||||||
|
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.flex_col()
|
||||||
|
.child(title_bar)
|
||||||
|
.child(dashboard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::Global for Global {}
|
||||||
|
|
||||||
|
pub fn current_theme<'a, E>(cx: &'a gpui::Context<E>) -> &'a theme::Theme {
|
||||||
|
&cx.global::<Global>().current_theme
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query_store<'a, E>(cx: &'a gpui::Context<E>) -> &'a query::Store {
|
||||||
|
&cx.global::<Global>().query_store
|
||||||
|
}
|
||||||
13
src/asset.rs
Normal file
13
src/asset.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
include!(concat!(env!("OUT_DIR"), "/asset.rs"));
|
||||||
|
|
||||||
|
pub struct Asset;
|
||||||
|
|
||||||
|
impl gpui::AssetSource for Asset {
|
||||||
|
fn load(&self, path: &str) -> gpui::Result<Option<std::borrow::Cow<'static, [u8]>>> {
|
||||||
|
load_asset(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list(&self, path: &str) -> gpui::Result<Vec<gpui::SharedString>> {
|
||||||
|
list_assets(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/asset/font_icon/chevron_down.svg
Normal file
1
src/asset/font_icon/chevron_down.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down-icon lucide-chevron-down"><path d="m6 9 6 6 6-6"/></svg>
|
||||||
|
After Width: | Height: | Size: 272 B |
1
src/asset/font_icon/folder_git.svg
Normal file
1
src/asset/font_icon/folder_git.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-folder-git2-icon lucide-folder-git-2"><path d="M18 19a5 5 0 0 1-5-5v8"/><path d="M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v5"/><circle cx="13" cy="12" r="2"/><circle cx="20" cy="19" r="2"/></svg>
|
||||||
|
After Width: | Height: | Size: 457 B |
80
src/colors.rs
Normal file
80
src/colors.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
use gpui::Rgba;
|
||||||
|
|
||||||
|
pub const fn hex(hex: u32) -> Rgba {
|
||||||
|
let [_, r, g, b] = hex.to_be_bytes();
|
||||||
|
|
||||||
|
Rgba {
|
||||||
|
r: r as f32 / 255.0,
|
||||||
|
g: g as f32 / 255.0,
|
||||||
|
b: b as f32 / 255.0,
|
||||||
|
a: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn neutral(shade: u16) -> Rgba {
|
||||||
|
match shade {
|
||||||
|
50 => hex(0xfafafa),
|
||||||
|
100 => hex(0xf5f5f5),
|
||||||
|
200 => hex(0xe5e5e5),
|
||||||
|
300 => hex(0xd4d4d4),
|
||||||
|
400 => hex(0xa3a3a3),
|
||||||
|
500 => hex(0x737373),
|
||||||
|
600 => hex(0x525252),
|
||||||
|
700 => hex(0x404040),
|
||||||
|
800 => hex(0x262626),
|
||||||
|
900 => hex(0x171717),
|
||||||
|
950 => hex(0x0a0a0a),
|
||||||
|
_ => panic!("unsupported Tailwind neutral shade"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn violet(shade: u16) -> Rgba {
|
||||||
|
match shade {
|
||||||
|
50 => hex(0xf5f3ff),
|
||||||
|
100 => hex(0xede9fe),
|
||||||
|
200 => hex(0xddd6fe),
|
||||||
|
300 => hex(0xc4b5fd),
|
||||||
|
400 => hex(0xa78bfa),
|
||||||
|
500 => hex(0x8b5cf6),
|
||||||
|
600 => hex(0x7c3aed),
|
||||||
|
700 => hex(0x6d28d9),
|
||||||
|
800 => hex(0x5b21b6),
|
||||||
|
900 => hex(0x4c1d95),
|
||||||
|
950 => hex(0x2e1065),
|
||||||
|
_ => panic!("unsupported Tailwind violet shade"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn amber(shade: u16) -> Rgba {
|
||||||
|
match shade {
|
||||||
|
50 => hex(0xfffbeb),
|
||||||
|
100 => hex(0xfef3c7),
|
||||||
|
200 => hex(0xfde68a),
|
||||||
|
300 => hex(0xfcd34d),
|
||||||
|
400 => hex(0xfbbf24),
|
||||||
|
500 => hex(0xf59e0b),
|
||||||
|
600 => hex(0xd97706),
|
||||||
|
700 => hex(0xb45309),
|
||||||
|
800 => hex(0x92400e),
|
||||||
|
900 => hex(0x78350f),
|
||||||
|
950 => hex(0x451a03),
|
||||||
|
_ => panic!("unsupported Tailwind amber shade"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn red(shade: u16) -> Rgba {
|
||||||
|
match shade {
|
||||||
|
50 => hex(0xfef2f2),
|
||||||
|
100 => hex(0xfee2e2),
|
||||||
|
200 => hex(0xfecaca),
|
||||||
|
300 => hex(0xfca5a5),
|
||||||
|
400 => hex(0xf87171),
|
||||||
|
500 => hex(0xef4444),
|
||||||
|
600 => hex(0xdc2626),
|
||||||
|
700 => hex(0xb91c1c),
|
||||||
|
800 => hex(0x991b1b),
|
||||||
|
900 => hex(0x7f1d1d),
|
||||||
|
950 => hex(0x450a0a),
|
||||||
|
_ => panic!("unsupported Tailwind red shade"),
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/component.rs
Normal file
2
src/component.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod font_icon;
|
||||||
|
pub mod text;
|
||||||
34
src/component/font_icon.rs
Normal file
34
src/component/font_icon.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use gpui::{Styled, svg};
|
||||||
|
use paste::paste;
|
||||||
|
|
||||||
|
use crate::app;
|
||||||
|
|
||||||
|
macro_rules! define_font_icons {
|
||||||
|
($($name:ident),+ $(,)?) => {
|
||||||
|
paste! {
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||||
|
pub enum FontIcon {
|
||||||
|
$($name),+
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn icon_path(icon: FontIcon) -> &'static str {
|
||||||
|
match icon {
|
||||||
|
$(
|
||||||
|
FontIcon::$name => concat!(
|
||||||
|
"asset/font_icon/",
|
||||||
|
stringify!([<$name:snake>]),
|
||||||
|
".svg"
|
||||||
|
),
|
||||||
|
)+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
define_font_icons!(ChevronDown, FolderGit);
|
||||||
|
|
||||||
|
pub fn font_icon<T>(icon: FontIcon, cx: &gpui::Context<T>) -> gpui::Svg {
|
||||||
|
let theme = cx.global::<app::Global>().current_theme;
|
||||||
|
svg().path(icon_path(icon)).text_color(theme.colors.text)
|
||||||
|
}
|
||||||
13
src/component/text.rs
Normal file
13
src/component/text.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use crate::app;
|
||||||
|
use gpui::{ParentElement, Styled, div};
|
||||||
|
|
||||||
|
pub trait Text: gpui::IntoElement {}
|
||||||
|
|
||||||
|
impl Text for &'static str {}
|
||||||
|
impl Text for String {}
|
||||||
|
impl Text for gpui::SharedString {}
|
||||||
|
|
||||||
|
pub fn text<'a, Content: Text, T>(s: Content, cx: &gpui::Context<T>) -> gpui::Div {
|
||||||
|
let theme = cx.global::<app::Global>().current_theme;
|
||||||
|
div().text_color(theme.colors.text).child(s)
|
||||||
|
}
|
||||||
48
src/dashboard.rs
Normal file
48
src/dashboard.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use gpui::{div, prelude::*};
|
||||||
|
|
||||||
|
use crate::theme::Variant;
|
||||||
|
|
||||||
|
pub struct Screen {
|
||||||
|
pub text: gpui::SharedString,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Screen {
|
||||||
|
fn render(
|
||||||
|
&mut self,
|
||||||
|
_window: &mut gpui::Window,
|
||||||
|
_cx: &mut gpui::Context<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let builtin = Variant::VioletDark;
|
||||||
|
let theme = builtin.theme();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.size_full()
|
||||||
|
.gap_3()
|
||||||
|
.bg(theme.colors.background)
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.shadow_lg()
|
||||||
|
.text_xl()
|
||||||
|
.text_color(theme.colors.text)
|
||||||
|
.child(format!("Hello, {}!", &self.text))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(theme.colors.text_muted)
|
||||||
|
.child(format!("Built-in theme: {}", builtin.label())),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(div().size_8().bg(theme.colors.surface))
|
||||||
|
.child(div().size_8().bg(theme.colors.surface_elevated))
|
||||||
|
.child(div().size_8().bg(theme.colors.accent))
|
||||||
|
.child(div().size_8().bg(theme.colors.success))
|
||||||
|
.child(div().size_8().bg(theme.colors.warning))
|
||||||
|
.child(div().size_8().bg(theme.colors.danger)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/main.rs
Normal file
45
src/main.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use gpui::{bounds, point, prelude::*, px, size};
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
mod asset;
|
||||||
|
mod colors;
|
||||||
|
mod component;
|
||||||
|
mod dashboard;
|
||||||
|
mod query;
|
||||||
|
mod theme;
|
||||||
|
mod titlebar;
|
||||||
|
mod api;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
gpui::Application::new()
|
||||||
|
.with_assets(asset::Asset)
|
||||||
|
.run(setup_application);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_application(cx: &mut gpui::App) {
|
||||||
|
let window_bounds = gpui::Bounds::centered(None, size(px(800.), px(600.0)), cx);
|
||||||
|
|
||||||
|
let global = app::Global {
|
||||||
|
safe_area: bounds(point(px(0.), px(0.)), size(px(72.), px(12.))),
|
||||||
|
current_theme: cx.window_appearance().into(),
|
||||||
|
query_store: query::Store::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let top_left = global.safe_area.origin;
|
||||||
|
|
||||||
|
cx.set_global(global);
|
||||||
|
|
||||||
|
cx.open_window(
|
||||||
|
gpui::WindowOptions {
|
||||||
|
window_bounds: Some(gpui::WindowBounds::Windowed(window_bounds)),
|
||||||
|
titlebar: Some(gpui::TitlebarOptions {
|
||||||
|
appears_transparent: true,
|
||||||
|
traffic_light_position: Some(top_left + point(px(8.), px(8.))),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|window, cx| cx.new(|cx| app::Chrome::new(window, cx)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
123
src/query.rs
Normal file
123
src/query.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
use crate::app;
|
||||||
|
use gpui::{AppContext, BorrowAppContext};
|
||||||
|
use std::any::Any;
|
||||||
|
|
||||||
|
pub trait QueryFn: Clone + 'static {
|
||||||
|
type Data: 'static;
|
||||||
|
type Error: 'static;
|
||||||
|
|
||||||
|
fn key(&self) -> &'static str;
|
||||||
|
|
||||||
|
async fn run(&self) -> Result<Self::Data, Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum QueryStatus<'a, Data, Error> {
|
||||||
|
Loading,
|
||||||
|
Loaded(&'a Data),
|
||||||
|
Err(&'a Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_query<F, T>(query_fn: F, cx: &mut gpui::Context<T>)
|
||||||
|
where
|
||||||
|
F: QueryFn,
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
let ent = cx.update_global::<app::Global, _>(|global, cx| {
|
||||||
|
let entity = global.query_store.ensure_query(&query_fn, cx);
|
||||||
|
|
||||||
|
if entity.read_with(cx, |state, _| matches!(state.data, QueryData::Pending)) {
|
||||||
|
global.query_store.execute_query(query_fn, entity.clone(), cx).detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
entity
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.observe(&ent, |_, _, cx| {
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= Store ==================
|
||||||
|
|
||||||
|
pub(crate) struct QueryState {
|
||||||
|
data: QueryData,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) enum QueryData {
|
||||||
|
Pending,
|
||||||
|
Loading,
|
||||||
|
Some(Box<dyn Any>),
|
||||||
|
Err(Box<dyn Any>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) type Entity = gpui::Entity<QueryState>;
|
||||||
|
|
||||||
|
pub struct Store {
|
||||||
|
query_data: std::collections::HashMap<String, Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Store {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
query_data: std::collections::HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_query<Q, T>(&mut self, query: &Q, cx: &mut gpui::Context<T>) -> Entity
|
||||||
|
where
|
||||||
|
Q: QueryFn,
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
self.query_data
|
||||||
|
.entry(query.key().into())
|
||||||
|
.or_insert_with(|| {
|
||||||
|
cx.new(|_| QueryState {
|
||||||
|
data: QueryData::Pending,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_query<Q, T>(
|
||||||
|
&mut self,
|
||||||
|
query: Q,
|
||||||
|
entity: Entity,
|
||||||
|
cx: &mut gpui::Context<T>,
|
||||||
|
) -> gpui::Task<anyhow::Result<()>>
|
||||||
|
where
|
||||||
|
Q: QueryFn,
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
entity.update(cx, |state, cx| {
|
||||||
|
state.data = QueryData::Loading;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn(async move |_, cx| {
|
||||||
|
let result = query.run().await;
|
||||||
|
|
||||||
|
entity.update(cx, |state, cx| {
|
||||||
|
state.data = match result {
|
||||||
|
Ok(data) => QueryData::Some(Box::new(data)),
|
||||||
|
Err(err) => QueryData::Err(Box::new(err)),
|
||||||
|
};
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_query<'a, Q, E>(&self, query_fn: Q, cx: &'a gpui::Context<E>) -> QueryStatus<'a, Q::Data, Q::Error> where Q: QueryFn {
|
||||||
|
let Some(ent) = self.query_data.get(query_fn.key()) else {
|
||||||
|
return QueryStatus::Loading;
|
||||||
|
};
|
||||||
|
let QueryState { data } = ent.read(cx);
|
||||||
|
match data {
|
||||||
|
QueryData::Loading | QueryData::Pending => QueryStatus::Loading,
|
||||||
|
QueryData::Some(data) => QueryStatus::Loaded(data.downcast_ref::<Q::Data>().unwrap()),
|
||||||
|
QueryData::Err(error) => QueryStatus::Err(error.downcast_ref::<Q::Error>().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/theme.rs
Normal file
117
src/theme.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use gpui::Rgba;
|
||||||
|
|
||||||
|
use crate::colors::{amber, hex, neutral, red, violet};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum ThemeMode {
|
||||||
|
Light,
|
||||||
|
#[default]
|
||||||
|
Dark,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Theme {
|
||||||
|
pub id: &'static str,
|
||||||
|
pub name: &'static str,
|
||||||
|
pub mode: ThemeMode,
|
||||||
|
pub colors: ThemeColors,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Theme {
|
||||||
|
fn default() -> Self {
|
||||||
|
Variant::default().theme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct ThemeColors {
|
||||||
|
pub background: Rgba,
|
||||||
|
pub surface: Rgba,
|
||||||
|
pub surface_elevated: Rgba,
|
||||||
|
pub border: Rgba,
|
||||||
|
pub text: Rgba,
|
||||||
|
pub text_muted: Rgba,
|
||||||
|
pub accent: Rgba,
|
||||||
|
pub accent_hover: Rgba,
|
||||||
|
pub accent_text: Rgba,
|
||||||
|
pub success: Rgba,
|
||||||
|
pub warning: Rgba,
|
||||||
|
pub danger: Rgba,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub enum Variant {
|
||||||
|
VioletLight,
|
||||||
|
#[default]
|
||||||
|
VioletDark,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Variant {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub const ALL: [Self; 2] = [Self::VioletLight, Self::VioletDark];
|
||||||
|
|
||||||
|
pub const fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::VioletLight => "Violet Light",
|
||||||
|
Self::VioletDark => "Violet Dark",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn theme(self) -> Theme {
|
||||||
|
match self {
|
||||||
|
Self::VioletLight => Theme {
|
||||||
|
id: "violet-light",
|
||||||
|
name: "Violet Light",
|
||||||
|
mode: ThemeMode::Light,
|
||||||
|
colors: ThemeColors {
|
||||||
|
background: neutral(50),
|
||||||
|
surface: neutral(100),
|
||||||
|
surface_elevated: neutral(200),
|
||||||
|
border: neutral(200),
|
||||||
|
text: neutral(900),
|
||||||
|
text_muted: neutral(600),
|
||||||
|
accent: violet(600),
|
||||||
|
accent_hover: violet(500),
|
||||||
|
accent_text: neutral(100),
|
||||||
|
success: hex(0x16a34a),
|
||||||
|
warning: amber(600),
|
||||||
|
danger: red(600),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Self::VioletDark => Theme {
|
||||||
|
id: "violet-dark",
|
||||||
|
name: "Violet Dark",
|
||||||
|
mode: ThemeMode::Dark,
|
||||||
|
colors: ThemeColors {
|
||||||
|
background: neutral(950),
|
||||||
|
surface: neutral(900),
|
||||||
|
surface_elevated: neutral(800),
|
||||||
|
border: neutral(800),
|
||||||
|
text: neutral(50),
|
||||||
|
text_muted: neutral(400),
|
||||||
|
accent: violet(400),
|
||||||
|
accent_hover: violet(300),
|
||||||
|
accent_text: neutral(100),
|
||||||
|
success: hex(0x22c55e),
|
||||||
|
warning: amber(500),
|
||||||
|
danger: red(500),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<gpui::WindowAppearance> for Theme {
|
||||||
|
fn from(value: gpui::WindowAppearance) -> Self {
|
||||||
|
let variant = match value {
|
||||||
|
gpui::WindowAppearance::Light | gpui::WindowAppearance::VibrantLight => {
|
||||||
|
Variant::VioletLight
|
||||||
|
}
|
||||||
|
gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => {
|
||||||
|
Variant::VioletDark
|
||||||
|
}
|
||||||
|
};
|
||||||
|
variant.theme()
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/titlebar.rs
Normal file
59
src/titlebar.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
use gpui::{ParentElement, Styled, div, AppContext};
|
||||||
|
|
||||||
|
use crate::{api, app, component::{
|
||||||
|
font_icon::{FontIcon, font_icon},
|
||||||
|
text::text,
|
||||||
|
}, query};
|
||||||
|
use crate::app::query_store;
|
||||||
|
use crate::query::use_query;
|
||||||
|
|
||||||
|
pub struct TitleBar {}
|
||||||
|
|
||||||
|
pub struct RepoSelector {}
|
||||||
|
|
||||||
|
impl gpui::Render for TitleBar {
|
||||||
|
fn render(
|
||||||
|
&mut self,
|
||||||
|
_window: &mut gpui::Window,
|
||||||
|
cx: &mut gpui::Context<Self>,
|
||||||
|
) -> impl gpui::IntoElement {
|
||||||
|
let g = cx.global::<app::Global>();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.w_full()
|
||||||
|
.h_8()
|
||||||
|
.flex()
|
||||||
|
.px(g.safe_area.size.width)
|
||||||
|
.py_2()
|
||||||
|
.flex_row()
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(g.current_theme.colors.border)
|
||||||
|
.bg(g.current_theme.colors.surface)
|
||||||
|
.text_color(g.current_theme.colors.text)
|
||||||
|
.child(repo_selector(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RepoSelector {
|
||||||
|
pub fn new(cx: &mut gpui::Context<Self>) -> Self {
|
||||||
|
use_query(api::repo::List, cx);
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repo_selector<T>(cx: &gpui::Context<T>) -> gpui::Div {
|
||||||
|
let store = app::query_store(cx);
|
||||||
|
let repo = store.read_query(api::repo::List, cx);
|
||||||
|
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.items_center()
|
||||||
|
.gap_1()
|
||||||
|
.text_xs()
|
||||||
|
.child(font_icon(FontIcon::FolderGit, cx).size_3())
|
||||||
|
.child(text("test/repo", cx))
|
||||||
|
.child(font_icon(FontIcon::ChevronDown, cx).size_3())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user