16
libclide/Cargo.lock
generated
Normal file
16
libclide/Cargo.lock
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "libclide"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
]
|
||||
12
libclide/Cargo.toml
Normal file
12
libclide/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "libclide"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
log = { workspace = true }
|
||||
devicons = { workspace = true }
|
||||
libclide-macros = { path = "../libclide-macros" }
|
||||
syntect = "5.3.0"
|
||||
24
libclide/src/fs.rs
Normal file
24
libclide/src/fs.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
pub mod entry_meta;
|
||||
|
||||
use anyhow::Context;
|
||||
pub use entry_meta::icon;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn read_file<P: AsRef<Path>>(p: P) -> anyhow::Result<String> {
|
||||
let path = p.as_ref();
|
||||
let meta =
|
||||
fs::metadata(path).unwrap_or_else(|_| panic!("Failed to get file metadata {path:?}"));
|
||||
if !meta.is_file() {
|
||||
crate::warn!(target:"FileSystem", "Attempted to open file {path:?} that is not a valid file");
|
||||
Err(anyhow::anyhow!(
|
||||
"Attempted to open file {path:?} that is not a valid file"
|
||||
))?;
|
||||
}
|
||||
let path_str = path.to_string_lossy().to_string();
|
||||
fs::read_to_string(path_str.as_str()).context(format!("Failed to read file {path:?}"))
|
||||
}
|
||||
64
libclide/src/fs/entry_meta.rs
Normal file
64
libclide/src/fs/entry_meta.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use devicons::FileIcon;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EntryMeta {
|
||||
pub abs_path: String,
|
||||
pub file_name: String,
|
||||
pub is_dir: bool,
|
||||
pub icon: FileIcon,
|
||||
}
|
||||
|
||||
impl EntryMeta {
|
||||
/// Normalizes a path, returning an absolute from the root of the filesystem.
|
||||
/// Does not resolve symlinks and extracts `./` or `../` segments.
|
||||
fn normalize<P: AsRef<Path>>(p: P) -> PathBuf {
|
||||
let path = p.as_ref();
|
||||
let mut buf = PathBuf::new();
|
||||
|
||||
for comp in path.components() {
|
||||
match comp {
|
||||
std::path::Component::ParentDir => {
|
||||
buf.pop();
|
||||
}
|
||||
std::path::Component::CurDir => {}
|
||||
_ => buf.push(comp),
|
||||
}
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn new<P: AsRef<Path>>(p: P) -> Result<Self> {
|
||||
let path = p.as_ref();
|
||||
let is_dir = path.is_dir();
|
||||
let abs_path = Self::normalize(path).to_string_lossy().to_string();
|
||||
let file_name = Path::new(&abs_path)
|
||||
.file_name()
|
||||
.context(format!("Failed to get file name for path: {abs_path:?}"))?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let icon = crate::fs::icon(&abs_path);
|
||||
Ok(EntryMeta {
|
||||
abs_path,
|
||||
file_name,
|
||||
is_dir,
|
||||
icon,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon<P: AsRef<str>>(p: P) -> FileIcon {
|
||||
let path = p.as_ref();
|
||||
if Path::new(&path).is_dir() {
|
||||
// Ensures directories are given a folder icon and not mistakenly resolved to a language.
|
||||
// For example, a directory named `cpp` would otherwise return a C++ icon.
|
||||
return FileIcon::from("dir/");
|
||||
}
|
||||
FileIcon::from(path)
|
||||
}
|
||||
7
libclide/src/lib.rs
Normal file
7
libclide/src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
pub mod fs;
|
||||
pub mod log;
|
||||
pub mod theme;
|
||||
10
libclide/src/log.rs
Normal file
10
libclide/src/log.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
pub mod macros;
|
||||
|
||||
pub use libclide_macros::Loggable;
|
||||
pub trait Loggable {
|
||||
const ID: &'static str;
|
||||
}
|
||||
75
libclide/src/log/macros.rs
Normal file
75
libclide/src/log/macros.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
//! Logging targets allow filtering of log messages by their source. By default, the log crate sets
|
||||
//! the target to the module path where the log macro was invoked if no target is provided.
|
||||
//!
|
||||
//! These macros essentially disable using the default target and instead require the target to be
|
||||
//! explicitly set. This is to avoid implicit pooling of log messages under the same default target,
|
||||
//! which can make it difficult to filter log messages by their source.
|
||||
//!
|
||||
//! The Loggable trait can be implemented to automatically associate log messages with a struct.
|
||||
//! ```
|
||||
//! use libclide::log::Loggable;
|
||||
//!
|
||||
//! #[derive(Loggable)]
|
||||
//! struct MyStruct;
|
||||
//! impl MyStruct {
|
||||
//! fn my_method(&self) {
|
||||
//! libclide::info!("This log message will use target <Self as Loggable>::ID, which is 'MyStruct'");
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! If the struct does not derive or implement Loggable, the target variant of the log macros must
|
||||
//! be used instead.
|
||||
//! ```
|
||||
//! libclide::info!(target: "CustomTarget", "This log message will have the target 'CustomTarget'");
|
||||
//! ```
|
||||
//!
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! info {
|
||||
(target: $target:expr, $($arg:tt)+) => ({
|
||||
log::info!(target: $target, $($arg)+)
|
||||
});
|
||||
|
||||
($($arg:tt)+) => (log::info!(target: Self::ID, $($arg)+))
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! debug {
|
||||
(target: $target:expr, $($arg:tt)+) => ({
|
||||
log::debug!(target: $target, $($arg)+)
|
||||
});
|
||||
|
||||
($($arg:tt)+) => (log::debug!(target: Self::ID, $($arg)+))
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! warn {
|
||||
(target: $target:expr, $($arg:tt)+) => ({
|
||||
log::warn!(target: $target, $($arg)+)
|
||||
});
|
||||
|
||||
($($arg:tt)+) => (log::warn!(target: Self::ID, $($arg)+))
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
(target: $target:expr, $($arg:tt)+) => ({
|
||||
log::error!(target: $target, $($arg)+)
|
||||
});
|
||||
|
||||
($($arg:tt)+) => (log::error!(target: Self::ID, $($arg)+))
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! trace {
|
||||
(target: $target:expr, $($arg:tt)+) => ({
|
||||
log::trace!(target: $target, $($arg)+)
|
||||
});
|
||||
|
||||
($($arg:tt)+) => (log::trace!(target: Self::ID, $($arg)+))
|
||||
}
|
||||
6
libclide/src/theme.rs
Normal file
6
libclide/src/theme.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
pub mod colors;
|
||||
pub mod highlighter;
|
||||
54
libclide/src/theme/colors.rs
Normal file
54
libclide/src/theme/colors.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
/// Colors shared between the TUI and GUI for the current theme.
|
||||
pub struct Colors {}
|
||||
impl Colors {
|
||||
pub const HOVERED: &str = "#303234";
|
||||
pub const UNHOVERED: &str = "#3c3f41";
|
||||
pub const PRESSED: &str = "#4b4f51";
|
||||
pub const MENUBAR: &str = "#262626";
|
||||
pub const MENUBAR_BORDER: &str = "#575757";
|
||||
pub const SCROLLBAR: &str = "#4b4f51";
|
||||
pub const SCROLLBAR_ACTIVE: &str = "#4b4f51";
|
||||
pub const SCROLLBAR_GUTTER: &str = "#3b3b3b";
|
||||
pub const LINENUMBER: &str = "#94989b";
|
||||
pub const ACTIVE: &str = "#d1d33f";
|
||||
pub const INACTIVE: &str = "#FFF";
|
||||
pub const EDITOR_BACKGROUND: &str = "#1E1F22";
|
||||
pub const EDITOR_TEXT: &str = "#acaea3";
|
||||
pub const EDITOR_HIGHLIGHTED_TEXT: &str = "#ccced3";
|
||||
pub const EDITOR_HIGHLIGHT: &str = "#ccced3";
|
||||
pub const GUTTER: &str = "#1e1f22";
|
||||
pub const EXPLORER_HOVERED: &str = "#4c5053";
|
||||
pub const EXPLORER_TEXT: &str = "#FFF";
|
||||
pub const EXPLORER_TEXT_SELECTED: &str = "#262626";
|
||||
pub const EXPLORER_BACKGROUND: &str = "#1E1F22";
|
||||
pub const EXPLORER_FOLDER: &str = "#54585b";
|
||||
pub const EXPLORER_FOLDER_OPEN: &str = "#393B40";
|
||||
pub const TERMINAL_BACKGROUND: &str = "#111111";
|
||||
pub const INFO_LOG: &str = "#C4FFFF";
|
||||
pub const DEBUG_LOG: &str = "#9148AF";
|
||||
pub const WARN_LOG: &str = "#C4A958";
|
||||
pub const ERROR_LOG: &str = "#ff5555";
|
||||
pub const TRACE_LOG: &str = "#ffaa00";
|
||||
|
||||
/// Converts a CSS hex color string (e.g., "#RRGGBB" or "#RGB") to u32 in 0x00RRGGBB format.
|
||||
pub fn css_to_u32(css: &str) -> u32 {
|
||||
let hex = css.trim_start_matches('#');
|
||||
// Expand shorthand #RGB to #RRGGBB
|
||||
let hex_full = match hex.len() {
|
||||
3 => hex
|
||||
.chars()
|
||||
.map(|c| std::iter::repeat_n(c, 2).collect::<String>())
|
||||
.collect::<String>(),
|
||||
6 => hex.to_string(),
|
||||
_ => panic!("Invalid hex color length: {hex:?}"),
|
||||
};
|
||||
// Parse the hex string as u32, masking to ensure the top (alpha) byte is 0x00.
|
||||
u32::from_str_radix(&hex_full, 16)
|
||||
.map(|rgb| rgb & 0x00FF_FFFF)
|
||||
.unwrap_or_else(|e| panic!("Failed to parse hex: {e:?}"))
|
||||
}
|
||||
}
|
||||
66
libclide/src/theme/highlighter.rs
Normal file
66
libclide/src/theme/highlighter.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::highlighting::ThemeSet;
|
||||
use syntect::html::{IncludeBackground, append_highlighted_html_for_styled_line};
|
||||
use syntect::parsing::SyntaxSet;
|
||||
use syntect::util::LinesWithEndings;
|
||||
|
||||
pub struct Highlighter {
|
||||
path: String,
|
||||
ss: SyntaxSet,
|
||||
ts: ThemeSet,
|
||||
}
|
||||
|
||||
impl Highlighter {
|
||||
pub fn new<P: AsRef<Path>>(p: P) -> anyhow::Result<Highlighter> {
|
||||
let path = p.as_ref();
|
||||
let meta =
|
||||
fs::metadata(path).unwrap_or_else(|_| panic!("Failed to get file metadata {path:?}"));
|
||||
let ss = SyntaxSet::load_defaults_nonewlines();
|
||||
let ts = ThemeSet::load_defaults();
|
||||
if !meta.is_file() {
|
||||
crate::error!(target:"FileSystem", "Attempted to open file {path:?} that is not a valid file");
|
||||
Err(anyhow::anyhow!(
|
||||
"Attempted to open file {path:?} that is not a valid file"
|
||||
))?;
|
||||
}
|
||||
Ok(Highlighter {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
ss,
|
||||
ts,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn syntax_highlight_text<P: AsRef<str>>(&self, p: P) -> String {
|
||||
let text = p.as_ref();
|
||||
let theme = &self.ts.themes["base16-ocean.dark"];
|
||||
let lang = self
|
||||
.ss
|
||||
.find_syntax_by_extension(
|
||||
Path::new(self.path.as_str())
|
||||
.extension()
|
||||
.map(|s| s.to_str())
|
||||
.unwrap_or_else(|| Some("md"))
|
||||
.expect("Failed to get file extension"),
|
||||
)
|
||||
.unwrap_or_else(|| self.ss.find_syntax_plain_text());
|
||||
let mut highlighter = HighlightLines::new(lang, theme);
|
||||
// If you care about the background, see `start_highlighted_html_snippet(theme);`.
|
||||
let mut output = String::from("<pre>\n");
|
||||
for line in LinesWithEndings::from(text) {
|
||||
let regions = highlighter
|
||||
.highlight_line(line, &self.ss)
|
||||
.expect("Failed to highlight");
|
||||
|
||||
append_highlighted_html_for_styled_line(
|
||||
®ions[..],
|
||||
IncludeBackground::No,
|
||||
&mut output,
|
||||
)
|
||||
.expect("Failed to insert highlighted html");
|
||||
}
|
||||
output.push_str("</pre>\n");
|
||||
output
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user