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