30 Commits

Author SHA1 Message Date
a63acb18fc WIP 2026-02-08 00:03:50 -05:00
f918d65888 Add clickable bread crumbs.
Clicking a parent path changes the root project directory.
2026-02-07 21:18:59 -05:00
aa42ec6072 Get file explorer icon based on file type.
Fixes #15
2026-02-07 18:41:51 -05:00
6f2a655497 Clean up context menus. 2026-02-07 12:59:44 -05:00
c0f38b531d Add arrow to show expanded folder. 2026-02-07 12:43:15 -05:00
4aad91416e Fix selection flickering when expanding folder. 2026-02-07 11:16:57 -05:00
ff1b5ab2e6 Hide handles when not in use. 2026-02-07 10:51:37 -05:00
491087a6c1 Add environment variables to config.toml 2026-02-07 10:37:05 -05:00
c170b3b20d WIP 2026-02-06 22:10:20 -05:00
125041f469 Add ClideEditorView. 2026-02-06 19:36:14 -05:00
67bf82d0cb Add qmllint.ini 2026-02-06 18:59:38 -05:00
39377b32f0 WIP 2026-02-06 18:59:02 -05:00
176efb97b7 Clean up ClideScrollBar. 2026-02-04 19:52:05 -05:00
755066d847 Clean up ClideHandle. 2026-02-04 19:29:52 -05:00
773d7818b5 Auto scroll logger. 2026-02-04 18:35:43 -05:00
7e58e3ee03 Split basic components for reuse. 2026-02-02 23:05:53 -05:00
0f50577d78 Fix scrollbar animations. 2026-02-02 18:21:28 -05:00
29024e3999 Format with qmlformat. 2026-02-02 18:08:37 -05:00
5af09485a3 Add trace logs. 2026-02-02 18:01:53 -05:00
e5b91eaed8 Add basic debug logger. 2026-02-01 20:23:23 -05:00
be383869b2 Add context menu on breadcrumbs.
The only option is to reset the root directory.
2026-02-01 19:10:15 -05:00
b9eee50e52 Fix menu bar colors. 2026-02-01 18:42:21 -05:00
4cc43916cb Update breadcrumbs when root directory changes. 2026-02-01 18:23:44 -05:00
a5bed9ed2c Fix panic when loading bad text in the GUI. 2026-02-01 17:15:21 -05:00
0fac2b71ab Fix TreeView nesting for ColumnLayout. 2026-02-01 15:35:40 -05:00
db2f878018 Fix about page image loading. 2026-02-01 13:44:43 -05:00
048d40eb83 Fix TreeView root index.
Parent folders will now be hidden based on the root folder selection in
the GUI.
2026-02-01 13:07:27 -05:00
c70bba16e4 Revert "Try to use QSortFilterProxyModel via cxx-qt."
This reverts commit 325cf285fc.
2026-02-01 13:07:10 -05:00
325cf285fc Try to use QSortFilterProxyModel via cxx-qt.
This may be easier if I just wrap a QAbstractItemModel in C++ and use
cxx-qt to pull it into QML? That way I could do C++ pointer things in
C++ and rust things in rust.
2026-01-31 21:47:19 -05:00
aa8590cd5c Add license headers. 2026-01-31 08:02:16 -05:00
50 changed files with 731 additions and 1358 deletions

View File

@@ -1,2 +1,6 @@
[build] [build]
rustflags = [ "-C", "link-arg=-fuse-ld=lld", ] rustflags = [ "-C", "link-arg=-fuse-ld=lld", ]
[env]
QMAKE="/opt/Qt/6.7.3/gcc_64/bin/qmake6"
LD_LIBRARY_PATH="/opt/Qt/6.7.3/gcc_64/lib"

View File

@@ -1,20 +0,0 @@
name: "Setup Qt"
description: "Install clide dependencies"
inputs:
qt-version:
description: "Qt version to install"
required: true
runs:
using: "composite"
steps:
- name: Install apt packages
run: |
sudo apt update -y
sudo apt install -y build-essential cmake curl libgl1-mesa-dev python3 python3-pip
shell: bash
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: ${{ inputs.qt-version }}

View File

@@ -1,97 +0,0 @@
name: Check
on:
push:
branches:
- '**'
tags:
- 'v*'
pull_request:
env:
QT_VERSION: 6.7.3
jobs:
Build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Qt
uses: ./.github/actions/setup-qt
with:
qt-version: ${{ env.QT_VERSION }}
- name: Build libclide
run: |
cargo b -p libclide --release
- name: Build libclide-macros
run: |
cargo b -p libclide-macros --release
- name: Build clide
run: |
cargo b --release
Test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Qt
uses: ./.github/actions/setup-qt
with:
qt-version: ${{ env.QT_VERSION }}
- name: Test libclide
run: |
cargo test -p libclide
- name: Test libclide-macros
run: |
cargo test -p libclide-macros
- name: Test clide
run: |
cargo test
Lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Qt
uses: ./.github/actions/setup-qt
with:
qt-version: ${{ env.QT_VERSION }}
- name: Lint libclide
run: |
cargo clippy -p libclide -- -D warnings
- name: Lint libclide-macros
run: |
cargo clippy -p libclide-macros -- -D warnings
- name: Lint clide
run: |
cargo clippy -- -D warnings
Format:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Qt
uses: ./.github/actions/setup-qt
with:
qt-version: ${{ env.QT_VERSION }}
- name: Format libclide
run: |
cargo fmt -p libclide --verbose -- --check
- name: Format libclide-macros
run: |
cargo fmt -p libclide-macros -- --check
- name: Format clide
run: |
cargo fmt --verbose -- --check

1
.gitignore vendored
View File

@@ -2,4 +2,3 @@
**/.qtcreator/** **/.qtcreator/**
**/.idea/** **/.idea/**
**/*.autosave/** **/*.autosave/**
**/.qmlls.ini

496
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,33 +3,22 @@ name = "clide"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[workspace]
resolver = "3"
members = [".", "libclide", "libclide-macros", ]
[workspace.dependencies]
anyhow = "1.0.100"
strum = "0.27.2"
log = { version = "0.4.27", features = [] }
devicons = "0.6.12"
[dependencies] [dependencies]
cxx = "1.0.95" cxx = "1.0.95"
cxx-qt = "0.8.0" cxx-qt = "0.8.0"
cxx-qt-lib = { version = "0.8.0", features = ["qt_full", "qt_gui", "qt_qml"] } cxx-qt-lib = { version = "0.8.0", features = ["qt_full", "qt_gui", "qt_qml"] }
log = { version = "0.4.27", features = [] }
dirs = "6.0.0" dirs = "6.0.0"
syntect = "5.2.0" syntect = "5.2.0"
clap = { version = "4.5.54", features = ["derive"] } clap = { version = "4.5.54", features = ["derive"] }
ratatui = "0.30.0" ratatui = "0.30.0"
anyhow = "1.0.100"
tui-tree-widget = "0.24.0" tui-tree-widget = "0.24.0"
tui-logger = "0.18.1" tui-logger = "0.18.1"
edtui = "0.11.1" edtui = "0.11.1"
libclide = { path = "./libclide" } strum = "0.27.2"
libclide-macros = { path = "./libclide-macros" } uuid = { version = "1.19.0", features = ["v4"] }
anyhow = { workspace = true } devicons = "0.6.12"
strum = { workspace = true }
log = { workspace = true }
devicons = { workspace = true }
[build-dependencies] [build-dependencies]
# The link_qt_object_files feature is required for statically linking Qt 6. # The link_qt_object_files feature is required for statically linking Qt 6.

View File

@@ -1,7 +1,5 @@
# CLIDE # CLIDE
[![Check](https://github.com/shaunrd0/clide/actions/workflows/check.yaml/badge.svg)](https://github.com/shaunrd0/clide/actions/workflows/check.yaml)
CLIDE is an extendable command-line driven development environment written in Rust using the Qt UI framework that supports both full and headless Linux environments. CLIDE is an extendable command-line driven development environment written in Rust using the Qt UI framework that supports both full and headless Linux environments.
The GUI is written in QML compiled through Rust using the cxx-qt crate, while the TUI was implemented using the ratatui crate. The GUI is written in QML compiled through Rust using the cxx-qt crate, while the TUI was implemented using the ratatui crate.
@@ -109,8 +107,6 @@ cargo run
clide clide
``` ```
![image](./resources/gui.png)
## Development ## Development
It's recommended to use RustRover or Qt Creator for development. It's recommended to use RustRover or Qt Creator for development.

View File

@@ -1,22 +1,21 @@
use cxx_qt_build::{CxxQtBuilder, QmlModule}; use cxx_qt_build::{CxxQtBuilder, QmlModule};
fn main() { fn main() {
CxxQtBuilder::new_qml_module(QmlModule::new("clide.module").qml_files([ CxxQtBuilder::new_qml_module(QmlModule::new("clide.module").qml_files(&[
"qml/ClideApplicationView.qml",
"qml/ClideEditorView.qml",
"qml/ClideExplorerView.qml",
"qml/ClideTreeView.qml",
"qml/Components/ClideAboutWindow.qml",
"qml/Components/ClideBreadCrumbs.qml",
"qml/Components/ClideEditor.qml",
"qml/Components/ClideHandle.qml",
"qml/Components/ClideLogger.qml",
"qml/Components/ClideMenu.qml",
"qml/Components/ClideMenuBar.qml",
"qml/Components/ClideMenuItem.qml",
"qml/Components/ClideScrollBar.qml",
"qml/Logger/Logger.qml",
"qml/main.qml", "qml/main.qml",
"qml/ClideAboutWindow.qml",
"qml/ClideTreeView.qml",
"qml/ClideProjectView.qml",
"qml/ClideEditor.qml",
"qml/ClideEditorView.qml",
"qml/ClideMenuBar.qml",
"qml/ClideLogger.qml",
"qml/Components/ClideScrollBar.qml",
"qml/Components/ClideHandle.qml",
"qml/Components/ClideMenu.qml",
"qml/Components/ClideMenuItem.qml",
"qml/Components/ClideBreadCrumbs.qml",
"qml/Logger/Logger.qml",
])) ]))
// Link Qt's Network library // Link Qt's Network library
// - Qt Core is always linked // - Qt Core is always linked

View File

@@ -1,12 +0,0 @@
[package]
name = "libclide-macros"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"

View File

@@ -1,24 +0,0 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
use proc_macro::TokenStream;
use quote::quote;
use syn::{ItemStruct, parse_macro_input};
#[proc_macro_derive(Loggable)]
pub fn loggable(item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemStruct);
let struct_name = &input.ident;
let generics = &input.generics;
let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
let struct_name_str = struct_name.to_string();
let expanded = quote! {
impl #impl_generics libclide::log::Loggable for #struct_name #type_generics #where_clause {
const ID: &'static str = #struct_name_str;
}
};
TokenStream::from(expanded)
}

16
libclide/Cargo.lock generated
View File

@@ -1,16 +0,0 @@
# 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",
]

View File

@@ -1,11 +0,0 @@
[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" }

View File

@@ -1,7 +0,0 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
pub mod entry_meta;
pub use entry_meta::icon;

View File

@@ -1,64 +0,0 @@
// 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)
}

View File

@@ -1,7 +0,0 @@
// 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;

View File

@@ -1,9 +0,0 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
pub mod macros;
pub trait Loggable {
const ID: &'static str;
}

View File

@@ -1,75 +0,0 @@
// 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_macros::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 as libclide::log::Loggable>::ID, $($arg)+))
}
#[macro_export]
macro_rules! debug {
(target: $target:expr, $($arg:tt)+) => ({
log::debug!(target: $target, $($arg)+)
});
($($arg:tt)+) => (log::debug!(target: <Self as libclide::log::Loggable>::ID, $($arg)+))
}
#[macro_export]
macro_rules! warn {
(target: $target:expr, $($arg:tt)+) => ({
log::warn!(target: $target, $($arg)+)
});
($($arg:tt)+) => (log::warn!(target: <Self as libclide::log::Loggable>::ID, $($arg)+))
}
#[macro_export]
macro_rules! error {
(target: $target:expr, $($arg:tt)+) => ({
log::error!(target: $target, $($arg)+)
});
($($arg:tt)+) => (log::error!(target: <Self as libclide::log::Loggable>::ID, $($arg)+))
}
#[macro_export]
macro_rules! trace {
(target: $target:expr, $($arg:tt)+) => ({
log::trace!(target: $target, $($arg)+)
});
($($arg:tt)+) => (log::trace!(target: <Self as libclide::log::Loggable>::ID, $($arg)+))
}

View File

@@ -1,5 +0,0 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
pub mod colors;

View File

@@ -1,54 +0,0 @@
// 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:?}"))
}
}

View File

@@ -1,46 +0,0 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import clide.module 1.0
import Logger 1.0
SplitView {
id: root
// Path to the directory of the project opened in clide.
required property string projectDir
anchors.fill: parent
// Customized handle to drag between the Navigation and the Editor.
handle: ClideHandle {
hovered: SplitHandle.hovered
pressed: SplitHandle.pressed
}
ClideExplorerView {
SplitView.fillHeight: true
SplitView.preferredWidth: 200
projectDir: root.projectDir
// Open files when clicked in the explorer.
onFileClicked: path => {
Logger.trace("Setting editor path from ClideExplorerView signal: " + path)
clideEditorView.filePath = path;
}
}
ClideEditorView {
id: clideEditorView
SplitView.fillHeight: true
SplitView.fillWidth: true
// Provide a path to the file currently open in the text editor.
// Initialized using the Default trait in Rust QML singleton FileSystem.
filePath: FileSystem.filePath
}
}

136
qml/ClideEditor.qml Normal file
View File

@@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import clide.module 1.0
import Logger 1.0
RowLayout {
// We use a flickable to synchronize the position of the editor and
// the line numbers. This is necessary because the line numbers can
// extend the available height.
Flickable {
id: lineNumbers
Layout.fillHeight: true
Layout.fillWidth: false
// Calculating the width correctly is important as the number grows.
// We need to ensure space required to show N line number digits.
// We use log10 to find how many digits are needed in a line number.
// log10(9) ~= .95; log10(10) = 1.0; log10(100) = 2.0 ...etc
// We +1 to ensure space for at least 1 digit, as floor(1.95) = 1.
// The +10 is additional spacing and can be adjusted.
Layout.preferredWidth: fontMetrics.averageCharacterWidth * (Math.floor(Math.log10(textArea.lineCount)) + 1) + 10
contentY: editorFlickable.contentY
interactive: false
Column {
anchors.fill: parent
topPadding: textArea.topPadding
Repeater {
id: repeatedLineNumbers
// TODO: Bug where text wrapping shows as new line number.
model: textArea.lineCount
// This Item is used for each line number in the gutter.
delegate: Item {
required property int index
// Calculates the height of each line in the text area.
height: textArea.contentHeight / textArea.lineCount
width: parent.width
// Show the line number.
Label {
id: numbers
color: RustColors.linenumber
font: textArea.font
height: parent.height
horizontalAlignment: Text.AlignLeft
text: parent.index + 1
verticalAlignment: Text.AlignVCenter
width: parent.width - indicator.width
background: Rectangle {
color: RustColors.terminal_background
}
}
// Draw edge along the right side of the line number.
Rectangle {
id: indicator
anchors.left: numbers.right
color: RustColors.linenumber
height: parent.height
width: 1
}
}
}
}
}
Flickable {
id: editorFlickable
Layout.fillHeight: true
Layout.fillWidth: true
boundsBehavior: Flickable.StopAtBounds
height: 650
ScrollBar.horizontal: ClideScrollBar {
}
ScrollBar.vertical: ClideScrollBar {
}
TextArea.flickable: TextArea {
id: textArea
antialiasing: true
focus: true
persistentSelection: true
selectByMouse: true
selectedTextColor: RustColors.editor_highlighted_text
selectionColor: RustColors.editor_highlight
text: FileSystem.readFile(root.filePath)
textFormat: Qt.AutoText
wrapMode: TextArea.Wrap
onLinkActivated: function (link) {
Qt.openUrlExternally(link);
}
// TODO: Handle saving
// Component.onCompleted: {
// if (Qt.application.arguments.length === 2)
// textDocument.source = "file:" + Qt.application.arguments[1]
// else
// textDocument.source = "qrc:/texteditor.html"
// }
// textDocument.onStatusChanged: {
// // a message lookup table using computed properties:
// // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer
// const statusMessages = {
// [ TextDocument.ReadError ]: qsTr("Failed to load “%1”"),
// [ TextDocument.WriteError ]: qsTr("Failed to save “%1”"),
// [ TextDocument.NonLocalFileError ]: qsTr("Not a local file: “%1”"),
// }
// const err = statusMessages[textDocument.status]
// if (err) {
// errorDialog.text = err.arg(textDocument.source)
// errorDialog.open()
// }
// }
}
FontMetrics {
id: fontMetrics
font: textArea.font
}
}
}

View File

@@ -9,26 +9,22 @@ import QtQuick.Layouts
import clide.module 1.0 import clide.module 1.0
import Logger 1.0 import Logger 1.0
Rectangle { SplitView {
id: root id: root
// The path to the file to show in the text editor. // The path to the file to show in the text editor.
// This is updated by a signal caught within ClideApplicationView. // This is updated by a signal caught within ClideProjectView.
// Initialized by the Default trait for the Rust QML singleton FileSystem.
required property string filePath required property string filePath
clip: true Layout.fillHeight: true
color: "transparent" Layout.fillWidth: true
radius: 20
SplitView {
anchors.fill: parent
orientation: Qt.Vertical orientation: Qt.Vertical
spacing: 3
// Customized handle to drag between the Editor and the Console. // Customized handle to drag between the Editor and the Console.
handle: ClideHandle { handle: ClideHandle {
hovered: SplitHandle.hovered
pressed: SplitHandle.pressed pressed: SplitHandle.pressed
hovered: SplitHandle.hovered
} }
Component.onCompleted: { Component.onCompleted: {
@@ -41,9 +37,10 @@ Rectangle {
} }
ClideEditor{ ClideEditor{
SplitView.preferredHeight: 650 id: clideEditor
} }
ClideLogger { ClideLogger {
} id: areaConsole
} }
} }

View File

@@ -1,62 +0,0 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import clide.module 1.0
import Logger 1.0
Rectangle {
id: root
required property string projectDir
signal fileClicked(string path)
clip: true
color: RustColors.explorer_background
topLeftRadius: 10
ColumnLayout {
anchors.fill: parent
spacing: 5
ClideBreadCrumbs {
id: breadCrumb
Layout.fillWidth: true
Layout.leftMargin: 15
Layout.rightMargin: 15
Layout.topMargin: 10
path: clideTreeView.rootDirectory
onCrumbClicked: path => {
Logger.trace("Crumb clicked: " + path);
clideTreeView.rootDirectory = path;
}
onResetRoot: {
clideTreeView.rootDirectory = clideTreeView.originalRootDirectory;
}
}
ClideTreeView {
id: clideTreeView
Layout.fillHeight: true
Layout.fillWidth: true
// Path to the directory opened in the file explorer.
originalRootDirectory: root.projectDir
rootDirectory: root.projectDir
// Pass the signal to the parent component using another signal.
onFileClicked: path => root.fileClicked(path)
onRootDirectoryChanged: {
Logger.log("Setting root directory: " + clideTreeView.rootDirectory);
breadCrumb.path = clideTreeView.rootDirectory;
}
}
}
}

View File

@@ -8,14 +8,15 @@ import QtQuick.Controls
import clide.module 1.0 import clide.module 1.0
import Logger 1.0 import Logger 1.0
Rectangle { Item {
color: RustColors.terminal_background
radius: 10
ListModel { ListModel {
id: model id: model
} }
Rectangle {
anchors.fill: parent
color: "#111"
}
ListView { ListView {
id: listView id: listView
@@ -37,6 +38,7 @@ Rectangle {
} }
anchors.fill: parent anchors.fill: parent
clip: true
model: model model: model
delegate: Text { delegate: Text {

View File

@@ -11,6 +11,7 @@ MenuBar {
// Background for this MenuBar. // Background for this MenuBar.
background: Rectangle { background: Rectangle {
color: RustColors.menubar color: RustColors.menubar
radius: 20.0
} }
// //

92
qml/ClideProjectView.qml Normal file
View File

@@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import clide.module 1.0
import Logger 1.0
SplitView {
id: root
// Path to the directory of the project opened in clide.
required property string projectDir
Layout.fillHeight: true
Layout.fillWidth: true
anchors.fill: parent
// Customized handle to drag between the Navigation and the Editor.
handle: Rectangle {
id: verticalSplitHandle
border.color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter
color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter
implicitWidth: 8
radius: 2.5
// Execute these behaviors when the color is changed.
Behavior on color {
ColorAnimation {
duration: 400
}
}
}
Rectangle {
id: navigationView
SplitView.fillHeight: true
SplitView.maximumWidth: 250
SplitView.minimumWidth: 0
SplitView.preferredWidth: 200
color: RustColors.explorer_background
radius: 20
ColumnLayout {
spacing: 2
ClideBreadCrumbs {
id: breadCrumb
Layout.bottomMargin: 5
Layout.leftMargin: 15
Layout.topMargin: 5
path: clideTreeView.rootDirectory
onCrumbClicked: path => {
Logger.trace("Crumb clicked: " + path);
clideTreeView.rootDirectory = path;
}
}
ClideTreeView {
id: clideTreeView
height: navigationView.height
// Path to the directory opened in the file explorer.
originalRootDirectory: root.projectDir
rootDirectory: root.projectDir
width: navigationView.width
onFileClicked: path => clideEditor.filePath = path
onRootDirectoryChanged: {
Logger.log("Setting root directory: " + clideTreeView.rootDirectory);
breadCrumb.path = clideTreeView.rootDirectory;
}
}
}
}
ClideEditorView {
id: clideEditor
SplitView.fillWidth: true
// Provide a path to the file currently open in the text editor.
// Initialized using the Default trait in Rust QML singleton FileSystem.
filePath: FileSystem.filePath
}
}

View File

@@ -10,30 +10,38 @@ import clide.module 1.0
import Logger 1.0 import Logger 1.0
TreeView { TreeView {
id: root id: fileSystemTreeView
property int lastIndex: -1 property int lastIndex: -1
required property string originalRootDirectory required property string originalRootDirectory
property string rootDirectory property string rootDirectory
property int rootIndent: 25
signal fileClicked(string filePath) signal fileClicked(string filePath)
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
boundsMovement: Flickable.StopAtBounds boundsMovement: Flickable.StopAtBounds
clip: true clip: true
leftMargin: 25
// The model is implemented in filesystem.rs
model: FileSystem model: FileSystem
// Set the root directory on the Rust model, returning the QModeIndex to use for the root of the tree view widget. rootIndex: FileSystem.setDirectory(fileSystemTreeView.rootDirectory)
rootIndex: FileSystem.setDirectory(root.rootDirectory)
// Provide our own custom ScrollIndicator for the TreeView. // Provide our own custom ScrollIndicator for the TreeView.
ScrollBar.horizontal: ClideScrollBar { ScrollIndicator.vertical: ScrollIndicator {
sizeModifier: 3 active: true
implicitWidth: 15
contentItem: Rectangle {
color: RustColors.scrollbar
implicitHeight: 6
implicitWidth: 6
opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0
Behavior on opacity {
OpacityAnimator {
duration: 500
}
}
} }
ScrollBar.vertical: ClideScrollBar {
sizeModifier: 3
} }
// The delegate represents a single entry in the filesystem. // The delegate represents a single entry in the filesystem.
@@ -45,40 +53,38 @@ TreeView {
required property int index required property int index
implicitHeight: 25 implicitHeight: 25
implicitWidth: root.width implicitWidth: fileSystemTreeView.width > 0 ? fileSystemTreeView.width : 250
indentation: 12 indentation: 12
background: Rectangle { background: Rectangle {
color: current ? RustColors.explorer_folder_open : "transparent" color: current ? RustColors.explorer_folder_open : "transparent"
radius: 20 radius: 2.5
width: root.width
} }
// Item name.
contentItem: Text { contentItem: Text {
anchors.left: itemIcon.right anchors.left: directoryIcon.right
anchors.leftMargin: 5 anchors.leftMargin: 5
color: RustColors.explorer_text color: RustColors.explorer_text
text: treeDelegate.fileName text: treeDelegate.fileName
} }
// Item Icon.
indicator: Label { indicator: Label {
id: itemIcon id: directoryIcon
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
antialiasing: true antialiasing: true
enabled: false
focus: false
font.family: localFont.font.family font.family: localFont.font.family
font.pixelSize: 18 font.pixelSize: 18
smooth: true smooth: true
// Get the icon from Rust implementation. text: fileSystemTreeView.model.icon(filePath)
text: root.model.icon(filePath) x: treeDelegate.leftMargin + (treeDelegate.depth * treeDelegate.indentation) + (indicator.visible ? indicator.width : 0)
x: root.rootIndent + (treeDelegate.depth * treeDelegate.indentation) + (carrotIndicator.visible ? carrotIndicator.width : 0)
} }
// Directory carrot indicator. FontLoader {
id: localFont
source: "qrc:/fonts/saucecodepro-xlight.ttf"
}
Label { Label {
id: carrotIndicator id: indicator
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
font.family: localFont.font.family font.family: localFont.font.family
@@ -86,11 +92,12 @@ TreeView {
font.weight: localFont.font.weight font.weight: localFont.font.weight
text: expanded ? "⮟" : "⮞" text: expanded ? "⮟" : "⮞"
visible: isTreeNode && hasChildren visible: isTreeNode && hasChildren
x: (root.rootIndent - implicitWidth) + (depth * indentation) x: padding + (depth * indentation)
} }
// Apply colorization effects to the icon for the item.
MultiEffect { MultiEffect {
anchors.fill: itemIcon id: iconOverlay
anchors.fill: directoryIcon
brightness: 1.0 brightness: 1.0
colorization: 1.0 colorization: 1.0
colorizationColor: { colorizationColor: {
@@ -103,7 +110,7 @@ TreeView {
else else
return RustColors.explorer_folder; return RustColors.explorer_folder;
} }
source: itemIcon source: directoryIcon
} }
HoverHandler { HoverHandler {
id: hoverHandler id: hoverHandler
@@ -117,10 +124,10 @@ TreeView {
switch (button) { switch (button) {
case Qt.LeftButton: case Qt.LeftButton:
if (treeDelegate.hasChildren) { if (treeDelegate.hasChildren) {
root.toggleExpanded(treeDelegate.row); fileSystemTreeView.toggleExpanded(treeDelegate.row);
} else { } else {
// If this model item doesn't have children, it means it's representing a file. // If this model item doesn't have children, it means it's representing a file.
root.fileClicked(treeDelegate.filePath); fileSystemTreeView.fileClicked(treeDelegate.filePath);
} }
break; break;
case Qt.RightButton: case Qt.RightButton:
@@ -139,7 +146,7 @@ TreeView {
onTriggered: { onTriggered: {
Logger.debug("Setting new root directory: " + treeDelegate.filePath); Logger.debug("Setting new root directory: " + treeDelegate.filePath);
root.rootDirectory = treeDelegate.filePath; fileSystemTreeView.rootDirectory = treeDelegate.filePath;
} }
} }
} }
@@ -148,8 +155,8 @@ TreeView {
text: qsTr("Reset root") text: qsTr("Reset root")
onTriggered: { onTriggered: {
Logger.log("Resetting root directory: " + root.originalRootDirectory); Logger.log("Resetting root directory: " + fileSystemTreeView.originalRootDirectory);
root.rootDirectory = root.originalRootDirectory; fileSystemTreeView.rootDirectory = fileSystemTreeView.originalRootDirectory;
} }
} }
} }
@@ -157,10 +164,4 @@ TreeView {
} }
selectionModel: ItemSelectionModel { selectionModel: ItemSelectionModel {
} }
FontLoader {
id: localFont
source: "qrc:/fonts/saucecodepro-xlight.ttf"
}
} }

View File

@@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
@@ -9,7 +5,7 @@ import QtQuick.Layouts 1.15
import clide.module 1.0 import clide.module 1.0
import Logger 1.0 import Logger 1.0
Rectangle { Item {
id: root id: root
property var fullPaths: [] property var fullPaths: []
@@ -17,30 +13,36 @@ Rectangle {
property var segments: [] property var segments: []
signal crumbClicked(string path) signal crumbClicked(string path)
signal resetRoot
function rebuildSegments(): string { function rebuildSegments() {
let cleaned = path; var cleaned = path;
if (cleaned.endsWith("/")) if (cleaned.endsWith("/"))
cleaned = cleaned.slice(0, -1); cleaned = cleaned.slice(0, -1);
var parts = cleaned.split("/");
root.segments = [];
root.fullPaths = [];
var current = "";
Logger.trace("Building segments for path: " + cleaned); Logger.trace("Building segments for path: " + cleaned);
segments = ["/"]; for (var i = 0; i < parts.length; ++i) {
fullPaths = ["/"]; if (parts[i] === "") {
let parts = cleaned.split("/"); current = "/";
let current = ""; root.segments.push("/");
// We already pushed the root `/` path during initialization, so skip index 0. root.fullPaths.push("/");
for (let i = 1; i < parts.length; ++i) { } else {
if (current === "/")
current += parts[i];
else
current += "/" + parts[i]; current += "/" + parts[i];
Logger.trace("Pushing path: " + parts[i] + " Current: " + current); Logger.trace("Pushing path: " + parts[i] + " Current: " + current);
segments.push(parts[i]); root.segments.push(parts[i]);
fullPaths.push(current); root.fullPaths.push(current);
} }
// Update the model used in the Repeater to show the new segments. }
repeater.model = segments; rep.model = root.segments;
} }
color: "transparent" anchors.leftMargin: 20
implicitHeight: breadcrumbRow.implicitHeight height: breadcrumbRow.implicitHeight
width: parent.width width: parent.width
Component.onCompleted: rebuildSegments() Component.onCompleted: rebuildSegments()
@@ -49,32 +51,28 @@ Rectangle {
Flow { Flow {
id: breadcrumbRow id: breadcrumbRow
anchors.fill: parent
spacing: 2
width: parent.width
Repeater { Repeater {
id: repeater id: rep
model: root.segments model: root.segments
delegate: Text { delegate: Text {
required property int index id: linkText
required property string modelData required property string modelData
function getText(): string { function getText() {
if (modelData === "/") { if (modelData === "/") {
return modelData; return modelData;
} }
Logger.trace("Getting valid text:" + modelData);
return modelData + "/"; return modelData + "/";
} }
// Show blue underlined hyperlink text if the mouse is hovering a segment.
color: mouseArea.containsMouse ? "#2a7fff" : RustColors.explorer_text color: mouseArea.containsMouse ? "#2a7fff" : RustColors.explorer_text
font.underline: mouseArea.containsMouse font.underline: mouseArea.containsMouse
text: getText() text: getText()
// Click events for each path segment call signal so the parent can set the file explorer root path.
MouseArea { MouseArea {
id: mouseArea id: mouseArea
@@ -82,8 +80,8 @@ Rectangle {
hoverEnabled: true hoverEnabled: true
onClicked: { onClicked: {
Logger.info(index + "] Breadcrumb clicked:" + root.fullPaths[index]); console.log("Breadcrumb clicked:", root.fullPaths[root.segments.indexOf(modelData)]);
crumbClicked(root.fullPaths[index]); root.crumbClicked(root.fullPaths[root.segments.indexOf(modelData)]);
} }
} }
} }
@@ -92,7 +90,7 @@ Rectangle {
TapHandler { TapHandler {
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
onSingleTapped: contextMenu.popup() onSingleTapped: (eventPoint, button) => contextMenu.popup()
} }
ClideMenu { ClideMenu {
id: contextMenu id: contextMenu
@@ -102,8 +100,8 @@ Rectangle {
text: qsTr("Reset root") text: qsTr("Reset root")
onTriggered: { onTriggered: {
Logger.info("Resetting root directory from ClideBreadCrumbs"); Logger.log("Resetting root directory: " + clideTreeView.originalRootDirectory);
resetRoot(); clideTreeView.rootDirectory = clideTreeView.originalRootDirectory;
} }
} }
} }

View File

@@ -1,116 +0,0 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import clide.module 1.0
import Logger 1.0
Rectangle {
color: RustColors.editor_background
RowLayout {
anchors.fill: parent
// We use a flickable to synchronize the position of the editor and
// the line numbers. This is necessary because the line numbers can
// extend the available height.
Flickable {
id: lineNumbers
Layout.fillHeight: true
Layout.fillWidth: false
// Calculating the width correctly is important as the number grows.
// We need to ensure space required to show N line number digits.
// We use log10 to find how many digits are needed in a line number.
// log10(9) ~= .95; log10(10) = 1.0; log10(100) = 2.0 ...etc
// We +1 to ensure space for at least 1 digit, as floor(1.95) = 1.
// The +10 is additional spacing and can be adjusted.
Layout.preferredWidth: fontMetrics.averageCharacterWidth * (Math.floor(Math.log10(textArea.lineCount)) + 1) + 10
contentY: editorFlickable.contentY
interactive: false
Column {
anchors.fill: parent
topPadding: textArea.topPadding
Repeater {
id: repeatedLineNumbers
// TODO: Bug where text wrapping shows as new line number.
model: textArea.lineCount
// This Item is used for each line number in the gutter.
delegate: Item {
required property int index
// Calculates the height of each line in the text area.
height: textArea.contentHeight / textArea.lineCount
width: parent.width
// Show the line number.
Label {
id: numbers
color: RustColors.linenumber
font: textArea.font
height: parent.height
horizontalAlignment: Text.AlignLeft
text: parent.index + 1
verticalAlignment: Text.AlignVCenter
width: parent.width - indicator.width
}
// Draw an edge along the right side of the line number.
Rectangle {
id: indicator
anchors.left: numbers.right
color: RustColors.linenumber
height: parent.height
width: 1
}
}
}
}
}
Flickable {
id: editorFlickable
Layout.fillHeight: true
Layout.fillWidth: true
boundsBehavior: Flickable.StopAtBounds
height: 650
ScrollBar.horizontal: ClideScrollBar {
}
ScrollBar.vertical: ClideScrollBar {
}
TextArea.flickable: TextArea {
id: textArea
antialiasing: true
focus: true
persistentSelection: true
selectByMouse: true
selectedTextColor: RustColors.editor_highlighted_text
selectionColor: RustColors.editor_highlight
text: FileSystem.readFile(root.filePath)
textFormat: Qt.AutoText
wrapMode: TextArea.Wrap
onLinkActivated: function (link) {
Qt.openUrlExternally(link);
}
}
FontMetrics {
id: fontMetrics
font: textArea.font
}
}
}
}

View File

@@ -16,7 +16,7 @@ Rectangle {
} else if (hovered) { } else if (hovered) {
return RustColors.hovered; return RustColors.hovered;
} else { } else {
return "transparent"; return "black";
} }
} }
required property bool hovered required property bool hovered
@@ -25,7 +25,6 @@ Rectangle {
border.color: currentColor border.color: currentColor
color: currentColor color: currentColor
implicitHeight: 8 implicitHeight: 8
implicitWidth: 8
radius: 2.5 radius: 2.5
opacity: root.hovered ? 1.0 : 0.0 opacity: root.hovered ? 1.0 : 0.0

View File

@@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick import QtQuick
import QtQuick.Controls.Basic import QtQuick.Controls.Basic

View File

@@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick import QtQuick
import QtQuick.Controls.Basic import QtQuick.Controls.Basic

View File

@@ -11,10 +11,9 @@ ScrollBar {
id: scrollBar id: scrollBar
// Height, opacitiy, width // Height, opacitiy, width
property int h: scrollBar.interactive ? sizeModifier * 2 : sizeModifier property int h: scrollBar.interactive ? 4 * 2 : 4
property int o: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.0 property int o: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.0
property int sizeModifier: 4 property int w: scrollBar.interactive ? 4 * 2 : 4
property int w: scrollBar.interactive ? sizeModifier * 2 : sizeModifier
// Scroll bar gutter // Scroll bar gutter
background: Rectangle { background: Rectangle {
@@ -26,7 +25,6 @@ ScrollBar {
// Fade the scrollbar gutter when inactive. // Fade the scrollbar gutter when inactive.
opacity: scrollBar.o opacity: scrollBar.o
radius: 20
Behavior on opacity { Behavior on opacity {
OpacityAnimator { OpacityAnimator {
@@ -57,7 +55,6 @@ ScrollBar {
// Fade the scrollbar when inactive. // Fade the scrollbar when inactive.
opacity: scrollBar.o opacity: scrollBar.o
radius: 20
// Smooth transition between color changes based on the state above. // Smooth transition between color changes based on the state above.
Behavior on color { Behavior on color {

View File

@@ -3,7 +3,3 @@ ClideHandle ClideHandle.qml
ClideMenu ClideMenu.qml ClideMenu ClideMenu.qml
ClideMenuItem ClideMenuItem.qml ClideMenuItem ClideMenuItem.qml
ClideBreadCrumbs ClideBreadCrumbs.qml ClideBreadCrumbs ClideBreadCrumbs.qml
ClideAboutWindow ClideAboutWindow.qml
ClideEditor ClideEditor.qml
ClideLogger ClideLogger.qml
ClideMenuBar ClideMenuBar.qml

View File

@@ -15,29 +15,22 @@ ApplicationWindow {
required property string appContextPath required property string appContextPath
height: 800 height: 800
title: "Clide" title: "CLIDE"
visible: true visible: true
width: 1200 width: 1200
menuBar: ClideMenuBar { menuBar: ClideMenuBar {
} }
Rectangle { MessageDialog {
color: RustColors.menubar id: errorDialog
width: appView.implicitWidth
height: appView.implicitHeight title: qsTr("Error")
}
ClideProjectView {
id: clideProjectView
ClideApplicationView {
id: appView
projectDir: appWindow.appContextPath projectDir: appWindow.appContextPath
implicitHeight: appWindow.height
implicitWidth: appWindow.width
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: 20
anchors.topMargin: 10
}
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 KiB

View File

@@ -5,12 +5,13 @@
use crate::AppContext; use crate::AppContext;
use anyhow::Result; use anyhow::Result;
use cxx_qt_lib::{QMapPair, QMapPair_QString_QVariant, QString, QVariant}; use cxx_qt_lib::{QMapPair, QMapPair_QString_QVariant, QString, QVariant};
use log::trace;
pub mod colors; pub mod colors;
pub mod filesystem; pub mod filesystem;
pub fn run(app_context: AppContext) -> Result<()> { pub fn run(app_context: AppContext) -> Result<()> {
libclide::trace!(target:"gui::run()", "Starting the GUI editor at {:?}", app_context.path); trace!(target:"gui::run()", "Starting the GUI editor at {:?}", app_context.path);
use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QUrl}; use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QUrl};

View File

@@ -2,10 +2,8 @@
// //
// SPDX-License-Identifier: GNU General Public License v3.0 or later // SPDX-License-Identifier: GNU General Public License v3.0 or later
use cxx_qt_lib::QColor;
use libclide::theme::colors::Colors;
#[cxx_qt::bridge] #[cxx_qt::bridge]
pub mod qobject { pub mod qobject {
unsafe extern "C++" { unsafe extern "C++" {
include!("cxx-qt-lib/qcolor.h"); include!("cxx-qt-lib/qcolor.h");
@@ -48,6 +46,8 @@ pub mod qobject {
} }
} }
use cxx_qt_lib::QColor;
pub struct RustColorsImpl { pub struct RustColorsImpl {
hovered: QColor, hovered: QColor,
unhovered: QColor, unhovered: QColor,
@@ -82,34 +82,34 @@ pub struct RustColorsImpl {
impl Default for RustColorsImpl { impl Default for RustColorsImpl {
fn default() -> Self { fn default() -> Self {
Self { Self {
hovered: QColor::try_from(Colors::HOVERED).unwrap(), hovered: QColor::try_from("#303234").unwrap(),
unhovered: QColor::try_from(Colors::UNHOVERED).unwrap(), unhovered: QColor::try_from("#3c3f41").unwrap(),
pressed: QColor::try_from(Colors::PRESSED).unwrap(), pressed: QColor::try_from("#4b4f51").unwrap(),
menubar: QColor::try_from(Colors::MENUBAR).unwrap(), menubar: QColor::try_from("#262626").unwrap(),
menubar_border: QColor::try_from(Colors::MENUBAR_BORDER).unwrap(), menubar_border: QColor::try_from("#575757").unwrap(),
scrollbar: QColor::try_from(Colors::SCROLLBAR).unwrap(), scrollbar: QColor::try_from("#4b4f51").unwrap(),
scrollbar_active: QColor::try_from(Colors::SCROLLBAR_ACTIVE).unwrap(), scrollbar_active: QColor::try_from("#4b4f51").unwrap(),
scrollbar_gutter: QColor::try_from(Colors::SCROLLBAR_GUTTER).unwrap(), scrollbar_gutter: QColor::try_from("#3b3b3b").unwrap(),
linenumber: QColor::try_from(Colors::LINENUMBER).unwrap(), linenumber: QColor::try_from("#94989b").unwrap(),
active: QColor::try_from(Colors::ACTIVE).unwrap(), active: QColor::try_from("#a9acb0").unwrap(),
inactive: QColor::try_from(Colors::INACTIVE).unwrap(), inactive: QColor::try_from("#FFF").unwrap(),
editor_background: QColor::try_from(Colors::EDITOR_BACKGROUND).unwrap(), editor_background: QColor::try_from("#111111").unwrap(),
editor_text: QColor::try_from(Colors::EDITOR_TEXT).unwrap(), editor_text: QColor::try_from("#acaea3").unwrap(),
editor_highlighted_text: QColor::try_from(Colors::EDITOR_HIGHLIGHTED_TEXT).unwrap(), editor_highlighted_text: QColor::try_from("#ccced3").unwrap(),
editor_highlight: QColor::try_from(Colors::EDITOR_HIGHLIGHT).unwrap(), editor_highlight: QColor::try_from("#ccced3").unwrap(),
gutter: QColor::try_from(Colors::GUTTER).unwrap(), gutter: QColor::try_from("#1e1f22").unwrap(),
explorer_hovered: QColor::try_from(Colors::EXPLORER_HOVERED).unwrap(), explorer_hovered: QColor::try_from("#4c5053").unwrap(),
explorer_text: QColor::try_from(Colors::EXPLORER_TEXT).unwrap(), explorer_text: QColor::try_from("#FFF").unwrap(),
explorer_text_selected: QColor::try_from(Colors::EXPLORER_TEXT_SELECTED).unwrap(), explorer_text_selected: QColor::try_from("#262626").unwrap(),
explorer_background: QColor::try_from(Colors::EXPLORER_BACKGROUND).unwrap(), explorer_background: QColor::try_from("#1E1F22").unwrap(),
explorer_folder: QColor::try_from(Colors::EXPLORER_FOLDER).unwrap(), explorer_folder: QColor::try_from("#54585b").unwrap(),
explorer_folder_open: QColor::try_from(Colors::EXPLORER_FOLDER_OPEN).unwrap(), explorer_folder_open: QColor::try_from("#393B40").unwrap(),
terminal_background: QColor::try_from(Colors::TERMINAL_BACKGROUND).unwrap(), terminal_background: QColor::try_from("#111111").unwrap(),
info_log: QColor::try_from(Colors::INFO_LOG).unwrap(), info_log: QColor::try_from("#C4FFFF").unwrap(),
debug_log: QColor::try_from(Colors::DEBUG_LOG).unwrap(), debug_log: QColor::try_from("#9148AF").unwrap(),
warn_log: QColor::try_from(Colors::WARN_LOG).unwrap(), warn_log: QColor::try_from("#C4A958").unwrap(),
error_log: QColor::try_from(Colors::ERROR_LOG).unwrap(), error_log: QColor::try_from("#ff5555").unwrap(),
trace_log: QColor::try_from(Colors::TRACE_LOG).unwrap(), trace_log: QColor::try_from("#ffaa00").unwrap(),
} }
} }
} }

View File

@@ -3,12 +3,16 @@
// SPDX-License-Identifier: GNU General Public License v3.0 or later // SPDX-License-Identifier: GNU General Public License v3.0 or later
use cxx_qt_lib::{QModelIndex, QString}; use cxx_qt_lib::{QModelIndex, QString};
use devicons::FileIcon;
use dirs; use dirs;
use log::warn;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use syntect::easy::HighlightLines; use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet; use syntect::highlighting::ThemeSet;
use syntect::html::{IncludeBackground, append_highlighted_html_for_styled_line}; use syntect::html::{
IncludeBackground, append_highlighted_html_for_styled_line, start_highlighted_html_snippet,
};
use syntect::parsing::SyntaxSet; use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings; use syntect::util::LinesWithEndings;
@@ -55,6 +59,7 @@ pub mod qobject {
} }
} }
// TODO: Implement a provider for QFileSystemModel::setIconProvider for icons.
pub struct FileSystemImpl { pub struct FileSystemImpl {
file_path: QString, file_path: QString,
} }
@@ -74,9 +79,9 @@ impl qobject::FileSystem {
return QString::default(); return QString::default();
} }
let meta = fs::metadata(path.to_string()) let meta = fs::metadata(path.to_string())
.unwrap_or_else(|_| panic!("Failed to get file metadata {path:?}")); .expect(format!("Failed to get file metadata {path:?}").as_str());
if !meta.is_file() { if !meta.is_file() {
libclide::warn!(target:"FileSystem", "Attempted to open file {path:?} that is not a valid file"); warn!(target:"FileSystem", "Attempted to open file {path:?} that is not a valid file");
return QString::default(); return QString::default();
} }
let path_str = path.to_string(); let path_str = path.to_string();
@@ -94,8 +99,7 @@ impl qobject::FileSystem {
) )
.unwrap_or_else(|| ss.find_syntax_plain_text()); .unwrap_or_else(|| ss.find_syntax_plain_text());
let mut highlighter = HighlightLines::new(lang, theme); let mut highlighter = HighlightLines::new(lang, theme);
// If you care about the background, see `start_highlighted_html_snippet(theme);`. let (mut output, _bg) = start_highlighted_html_snippet(theme);
let mut output = String::from("<pre>\n");
for line in LinesWithEndings::from(lines.as_str()) { for line in LinesWithEndings::from(lines.as_str()) {
let regions = highlighter let regions = highlighter
.highlight_line(line, &ss) .highlight_line(line, &ss)
@@ -112,7 +116,7 @@ impl qobject::FileSystem {
output.push_str("</pre>\n"); output.push_str("</pre>\n");
QString::from(output) QString::from(output)
} else { } else {
QString::default() return QString::default();
} }
} }
@@ -124,7 +128,7 @@ impl qobject::FileSystem {
fn set_directory(self: std::pin::Pin<&mut Self>, path: &QString) -> QModelIndex { fn set_directory(self: std::pin::Pin<&mut Self>, path: &QString) -> QModelIndex {
if !path.is_empty() if !path.is_empty()
&& fs::metadata(path.to_string()) && fs::metadata(path.to_string())
.unwrap_or_else(|_| panic!("Failed to get metadata for path {path:?}")) .expect(format!("Failed to get metadata for path {path:?}").as_str())
.is_dir() .is_dir()
{ {
self.set_root_path(path) self.set_root_path(path)
@@ -141,6 +145,13 @@ impl qobject::FileSystem {
} }
fn icon(self: std::pin::Pin<&mut Self>, path: &QString) -> QString { fn icon(self: std::pin::Pin<&mut Self>, path: &QString) -> QString {
QString::from(libclide::fs::icon(path.to_string().as_str()).to_string()) let str = path.to_string();
if Path::new(&str).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 QString::from(FileIcon::from("dir/").to_string())
}
let icon = FileIcon::from(str);
QString::from(icon.to_string())
} }
} }

View File

@@ -4,6 +4,7 @@
use anyhow::{Context, Result, anyhow}; use anyhow::{Context, Result, anyhow};
use clap::Parser; use clap::Parser;
use log::{info, trace};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
pub mod gui; pub mod gui;
@@ -55,7 +56,7 @@ impl AppContext {
// If no path was provided, use the current directory. // If no path was provided, use the current directory.
None => std::env::current_dir().context("Failed to obtain current directory")?, None => std::env::current_dir().context("Failed to obtain current directory")?,
}; };
libclide::info!(target:"main()", "Root path detected: {path:?}"); info!(target:"main()", "Root path detected: {path:?}");
Ok(Self { Ok(Self {
path, path,
@@ -79,9 +80,9 @@ fn main() -> Result<()> {
RunMode::GuiAttached => gui::run(app_context), RunMode::GuiAttached => gui::run(app_context),
RunMode::Tui => tui::run(app_context), RunMode::Tui => tui::run(app_context),
RunMode::Gui => { RunMode::Gui => {
libclide::trace!(target:"main()", "Starting GUI in a new process"); trace!(target:"main()", "Starting GUI in a new process");
Command::new(std::env::current_exe()?) Command::new(std::env::current_exe()?)
.args(["--gui", app_context.path.to_str().unwrap()]) .args(&["--gui", app_context.path.to_str().unwrap()])
.stdout(Stdio::null()) .stdout(Stdio::null())
.stderr(Stdio::null()) .stderr(Stdio::null())
.stdin(Stdio::null()) .stdin(Stdio::null())

View File

@@ -13,8 +13,7 @@ mod menu_bar;
use crate::AppContext; use crate::AppContext;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use libclide_macros::Loggable; use log::{LevelFilter, debug, info, trace};
use log::LevelFilter;
use ratatui::Terminal; use ratatui::Terminal;
use ratatui::backend::CrosstermBackend; use ratatui::backend::CrosstermBackend;
use ratatui::crossterm::event::{ use ratatui::crossterm::event::{
@@ -29,22 +28,24 @@ use tui_logger::{
TuiLoggerFile, TuiLoggerLevelOutput, init_logger, set_default_level, set_log_file, TuiLoggerFile, TuiLoggerLevelOutput, init_logger, set_default_level, set_log_file,
}; };
#[derive(Loggable)]
struct Tui { struct Tui {
terminal: Terminal<CrosstermBackend<Stdout>>, terminal: Terminal<CrosstermBackend<Stdout>>,
root_path: std::path::PathBuf, root_path: std::path::PathBuf,
} }
pub fn run(app_context: AppContext) -> Result<()> { pub fn run(app_context: AppContext) -> Result<()> {
libclide::trace!(target: "clide::tui::run", "Starting TUI"); trace!(target:Tui::ID, "Starting TUI");
Tui::new(app_context)?.start() Tui::new(app_context)?.start()
} }
impl Tui { impl Tui {
pub const ID: &str = "Tui";
fn new(app_context: AppContext) -> Result<Self> { fn new(app_context: AppContext) -> Result<Self> {
trace!(target:Self::ID, "Building {}", Self::ID);
init_logger(LevelFilter::Trace)?; init_logger(LevelFilter::Trace)?;
set_default_level(LevelFilter::Trace); set_default_level(LevelFilter::Trace);
libclide::debug!("Logging initialized"); debug!(target:Self::ID, "Logging initialized");
let mut dir = env::temp_dir(); let mut dir = env::temp_dir();
dir.push("clide.log"); dir.push("clide.log");
@@ -56,7 +57,7 @@ impl Tui {
.output_file(false) .output_file(false)
.output_separator(':'); .output_separator(':');
set_log_file(file_options); set_log_file(file_options);
libclide::debug!("Logging to file: {dir:?}"); debug!(target:Self::ID, "Logging to file: {dir:?}");
Ok(Self { Ok(Self {
terminal: Terminal::new(CrosstermBackend::new(stdout()))?, terminal: Terminal::new(CrosstermBackend::new(stdout()))?,
@@ -65,7 +66,7 @@ impl Tui {
} }
fn start(self) -> Result<()> { fn start(self) -> Result<()> {
libclide::info!("Starting the TUI editor at {:?}", self.root_path); info!(target:Self::ID, "Starting the TUI editor at {:?}", self.root_path);
ratatui::crossterm::execute!( ratatui::crossterm::execute!(
stdout(), stdout(),
EnterAlternateScreen, EnterAlternateScreen,
@@ -82,7 +83,7 @@ impl Tui {
} }
fn stop() -> Result<()> { fn stop() -> Result<()> {
libclide::info!("Stopping the TUI editor"); info!(target:Self::ID, "Stopping the TUI editor");
disable_raw_mode()?; disable_raw_mode()?;
ratatui::crossterm::execute!( ratatui::crossterm::execute!(
stdout(), stdout(),

View File

@@ -2,18 +2,19 @@
// //
// SPDX-License-Identifier: GNU General Public License v3.0 or later // SPDX-License-Identifier: GNU General Public License v3.0 or later
use libclide_macros::Loggable;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::text::{Line, Span}; use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap}; use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap};
#[derive(Loggable)]
pub struct About {} pub struct About {}
impl About { impl About {
#[allow(unused)]
pub const ID: &str = "About";
pub fn new() -> Self { pub fn new() -> Self {
// libclide::trace!("Building {}", Self::id()); // trace!(target:Self::id(), "Building {}", Self::id());
Self {} Self {}
} }
} }
@@ -67,8 +68,8 @@ impl Widget for About {
.map(|l| Line::from(Span::raw(*l))) .map(|l| Line::from(Span::raw(*l)))
.collect(); .collect();
Clear.render(kilroy_rect, buf); Clear::default().render(kilroy_rect, buf);
Clear.render(chunks[1], buf); Clear::default().render(chunks[1], buf);
Paragraph::new(about_lines) Paragraph::new(about_lines)
.block( .block(
Block::default() Block::default()

View File

@@ -3,14 +3,15 @@
// SPDX-License-Identifier: GNU General Public License v3.0 or later // SPDX-License-Identifier: GNU General Public License v3.0 or later
use crate::tui::about::About; use crate::tui::about::About;
use crate::tui::app::AppComponent::{AppEditor, AppExplorer, AppLogger};
use crate::tui::component::{Action, Component, Focus, FocusState, Visibility, VisibleState}; use crate::tui::component::{Action, Component, Focus, FocusState, Visibility, VisibleState};
use crate::tui::editor_tab::EditorTab; use crate::tui::editor_tab::EditorTab;
use crate::tui::explorer::Explorer; use crate::tui::explorer::Explorer;
use crate::tui::logger::Logger; use crate::tui::logger::Logger;
use crate::tui::menu_bar::MenuBar; use crate::tui::menu_bar::MenuBar;
use AppComponent::AppMenuBar;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use libclide::log::Loggable; use log::{error, info, trace};
use libclide_macros::Loggable;
use ratatui::DefaultTerminal; use ratatui::DefaultTerminal;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event; use ratatui::crossterm::event;
@@ -25,13 +26,12 @@ use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum AppComponent { pub enum AppComponent {
Editor, AppEditor,
Explorer, AppExplorer,
Logger, AppLogger,
MenuBar, AppMenuBar,
} }
#[derive(Loggable)]
pub struct App<'a> { pub struct App<'a> {
editor_tab: EditorTab, editor_tab: EditorTab,
explorer: Explorer<'a>, explorer: Explorer<'a>,
@@ -42,14 +42,16 @@ pub struct App<'a> {
} }
impl<'a> App<'a> { impl<'a> App<'a> {
pub const ID: &'static str = "App";
pub fn new(root_path: PathBuf) -> Result<Self> { pub fn new(root_path: PathBuf) -> Result<Self> {
libclide::trace!("Building {}", <Self as Loggable>::ID); trace!(target:Self::ID, "Building {}", Self::ID);
let app = Self { let app = Self {
editor_tab: EditorTab::new(), editor_tab: EditorTab::new(),
explorer: Explorer::new(&root_path)?, explorer: Explorer::new(&root_path)?,
logger: Logger::new(), logger: Logger::new(),
menu_bar: MenuBar::new(), menu_bar: MenuBar::new(),
last_active: AppComponent::Editor, last_active: AppEditor,
about: false, about: false,
}; };
Ok(app) Ok(app)
@@ -57,13 +59,13 @@ impl<'a> App<'a> {
/// Logic that should be executed once on application startup. /// Logic that should be executed once on application startup.
pub fn start(&mut self) -> Result<()> { pub fn start(&mut self) -> Result<()> {
libclide::trace!("Starting App"); trace!(target:Self::ID, "Starting App");
Ok(()) Ok(())
} }
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
self.start()?; self.start()?;
libclide::trace!("Entering App run loop"); trace!(target:Self::ID, "Entering App run loop");
loop { loop {
terminal.draw(|f| { terminal.draw(|f| {
f.render_widget(&mut self, f.area()); f.render_widget(&mut self, f.area());
@@ -85,18 +87,18 @@ impl<'a> App<'a> {
fn draw_bottom_status(&self, area: Rect, buf: &mut Buffer) { fn draw_bottom_status(&self, area: Rect, buf: &mut Buffer) {
// Determine help text from the most recently focused component. // Determine help text from the most recently focused component.
let help = match self.last_active { let help = match self.last_active {
AppComponent::Editor => match self.editor_tab.current_editor() { AppEditor => match self.editor_tab.current_editor() {
Some(editor) => editor.component_state.help_text.clone(), Some(editor) => editor.component_state.help_text.clone(),
None => { None => {
if !self.editor_tab.is_empty() { if !self.editor_tab.is_empty() {
libclide::error!("Failed to get Editor while drawing bottom status bar"); error!(target:Self::ID, "Failed to get Editor while drawing bottom status bar");
} }
"Failed to get current Editor while getting widget help text".to_string() "Failed to get current Editor while getting widget help text".to_string()
} }
}, },
AppComponent::Explorer => self.explorer.component_state.help_text.clone(), AppExplorer => self.explorer.component_state.help_text.clone(),
AppComponent::Logger => self.logger.component_state.help_text.clone(), AppLogger => self.logger.component_state.help_text.clone(),
AppComponent::MenuBar => self.menu_bar.component_state.help_text.clone(), AppMenuBar => self.menu_bar.component_state.help_text.clone(),
}; };
Paragraph::new( Paragraph::new(
concat!( concat!(
@@ -113,32 +115,32 @@ impl<'a> App<'a> {
} }
fn clear_focus(&mut self) { fn clear_focus(&mut self) {
libclide::info!("Clearing all widget focus"); info!(target:Self::ID, "Clearing all widget focus");
self.explorer.component_state.set_focus(Focus::Inactive); self.explorer.component_state.set_focus(Focus::Inactive);
self.explorer.component_state.set_focus(Focus::Inactive); self.explorer.component_state.set_focus(Focus::Inactive);
self.logger.component_state.set_focus(Focus::Inactive); self.logger.component_state.set_focus(Focus::Inactive);
self.menu_bar.component_state.set_focus(Focus::Inactive); self.menu_bar.component_state.set_focus(Focus::Inactive);
match self.editor_tab.current_editor_mut() { match self.editor_tab.current_editor_mut() {
None => { None => {
libclide::error!("Failed to get current Editor while clearing focus") error!(target:Self::ID, "Failed to get current Editor while clearing focus")
} }
Some(editor) => editor.component_state.set_focus(Focus::Inactive), Some(editor) => editor.component_state.set_focus(Focus::Inactive),
} }
} }
fn change_focus(&mut self, focus: AppComponent) { fn change_focus(&mut self, focus: AppComponent) {
libclide::info!("Changing widget focus to {:?}", focus); info!(target:Self::ID, "Changing widget focus to {:?}", focus);
self.clear_focus(); self.clear_focus();
match focus { match focus {
AppComponent::Editor => match self.editor_tab.current_editor_mut() { AppEditor => match self.editor_tab.current_editor_mut() {
None => { None => {
libclide::error!("Failed to get current Editor while changing focus") error!(target:Self::ID, "Failed to get current Editor while changing focus")
} }
Some(editor) => editor.component_state.set_focus(Focus::Active), Some(editor) => editor.component_state.set_focus(Focus::Active),
}, },
AppComponent::Explorer => self.explorer.component_state.set_focus(Focus::Active), AppExplorer => self.explorer.component_state.set_focus(Focus::Active),
AppComponent::Logger => self.logger.component_state.set_focus(Focus::Active), AppLogger => self.logger.component_state.set_focus(Focus::Active),
AppComponent::MenuBar => self.menu_bar.component_state.set_focus(Focus::Active), AppMenuBar => self.menu_bar.component_state.set_focus(Focus::Active),
} }
self.last_active = focus; self.last_active = focus;
} }
@@ -253,37 +255,35 @@ impl<'a> Component for App<'a> {
} }
// Handle events for all components. // Handle events for all components.
let action = match self.last_active { let action = match self.last_active {
AppComponent::Editor => self.editor_tab.handle_event(event.clone())?, AppEditor => self.editor_tab.handle_event(event.clone())?,
AppComponent::Explorer => self.explorer.handle_event(event.clone())?, AppExplorer => self.explorer.handle_event(event.clone())?,
AppComponent::Logger => self.logger.handle_event(event.clone())?, AppLogger => self.logger.handle_event(event.clone())?,
AppComponent::MenuBar => self.menu_bar.handle_event(event.clone())?, AppMenuBar => self.menu_bar.handle_event(event.clone())?,
}; };
// Components should always handle mouse events for click interaction. // Components should always handle mouse events for click interaction.
if let Some(mouse) = event.as_mouse_event() if let Some(mouse) = event.as_mouse_event() {
&& mouse.kind == MouseEventKind::Down(MouseButton::Left) if mouse.kind == MouseEventKind::Down(MouseButton::Left) {
{
if let Some(editor) = self.editor_tab.current_editor_mut() { if let Some(editor) = self.editor_tab.current_editor_mut() {
editor.handle_mouse_events(mouse)?; editor.handle_mouse_events(mouse)?;
} }
self.explorer.handle_mouse_events(mouse)?; self.explorer.handle_mouse_events(mouse)?;
self.logger.handle_mouse_events(mouse)?; self.logger.handle_mouse_events(mouse)?;
} }
}
// Handle actions returned from widgets that may need context on other widgets or app state. // Handle actions returned from widgets that may need context on other widgets or app state.
match action { match action {
Action::Quit | Action::Handled => Ok(action), Action::Quit | Action::Handled => Ok(action),
Action::Save => match self.editor_tab.current_editor_mut() { Action::Save => match self.editor_tab.current_editor_mut() {
None => { None => {
libclide::error!( error!(target:Self::ID, "Failed to get current editor while handling App Action::Save");
"Failed to get current editor while handling App Action::Save"
);
Ok(Action::Noop) Ok(Action::Noop)
} }
Some(editor) => match editor.save() { Some(editor) => match editor.save() {
Ok(_) => Ok(Action::Handled), Ok(_) => Ok(Action::Handled),
Err(e) => { Err(e) => {
libclide::error!("Failed to save editor contents: {e}"); error!(target:Self::ID, "Failed to save editor contents: {e}");
Ok(Action::Noop) Ok(Action::Noop)
} }
}, },
@@ -302,16 +302,14 @@ impl<'a> Component for App<'a> {
Err(_) => Ok(Action::Noop), Err(_) => Ok(Action::Noop),
}, },
Action::ReloadFile => { Action::ReloadFile => {
libclide::trace!("Reloading file for current editor"); trace!(target:Self::ID, "Reloading file for current editor");
if let Some(editor) = self.editor_tab.current_editor_mut() { if let Some(editor) = self.editor_tab.current_editor_mut() {
editor editor
.reload_contents() .reload_contents()
.map(|_| Action::Handled) .map(|_| Action::Handled)
.context("Failed to handle Action::ReloadFile") .context("Failed to handle Action::ReloadFile")
} else { } else {
libclide::error!( error!(target:Self::ID, "Failed to get current editor while handling App Action::ReloadFile");
"Failed to get current editor while handling App Action::ReloadFile"
);
Ok(Action::Noop) Ok(Action::Noop)
} }
} }
@@ -351,7 +349,7 @@ impl<'a> Component for App<'a> {
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
state: _state, state: _state,
} => { } => {
self.change_focus(AppComponent::Explorer); self.change_focus(AppExplorer);
Ok(Action::Handled) Ok(Action::Handled)
} }
KeyEvent { KeyEvent {
@@ -360,7 +358,7 @@ impl<'a> Component for App<'a> {
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
state: _state, state: _state,
} => { } => {
self.change_focus(AppComponent::Editor); self.change_focus(AppEditor);
Ok(Action::Handled) Ok(Action::Handled)
} }
KeyEvent { KeyEvent {
@@ -369,7 +367,7 @@ impl<'a> Component for App<'a> {
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
state: _state, state: _state,
} => { } => {
self.change_focus(AppComponent::Logger); self.change_focus(AppLogger);
Ok(Action::Handled) Ok(Action::Handled)
} }
KeyEvent { KeyEvent {
@@ -378,7 +376,7 @@ impl<'a> Component for App<'a> {
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
state: _state, state: _state,
} => { } => {
self.change_focus(AppComponent::MenuBar); self.change_focus(AppMenuBar);
Ok(Action::Handled) Ok(Action::Handled)
} }
KeyEvent { KeyEvent {

View File

@@ -7,8 +7,7 @@
use crate::tui::component::Focus::Inactive; use crate::tui::component::Focus::Inactive;
use Focus::Active; use Focus::Active;
use anyhow::Result; use anyhow::Result;
use libclide::theme::colors::Colors; use log::trace;
use libclide_macros::Loggable;
use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent}; use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent};
use ratatui::style::Color; use ratatui::style::Color;
@@ -62,7 +61,7 @@ pub trait Component {
} }
} }
#[derive(Debug, Clone, Default, Loggable)] #[derive(Debug, Clone, Default)]
pub struct ComponentState { pub struct ComponentState {
pub(crate) focus: Focus, pub(crate) focus: Focus,
pub(crate) vis: Visibility, pub(crate) vis: Visibility,
@@ -75,7 +74,7 @@ impl ComponentState {
} }
fn new() -> Self { fn new() -> Self {
libclide::trace!(target:Self::id(), "Building {}", Self::id()); trace!(target:Self::id(), "Building {}", Self::id());
Self { Self {
focus: Active, focus: Active,
vis: Visibility::Visible, vis: Visibility::Visible,
@@ -99,8 +98,8 @@ pub enum Focus {
impl Focus { impl Focus {
pub(crate) fn get_active_color(&self) -> Color { pub(crate) fn get_active_color(&self) -> Color {
match self { match self {
Active => Color::from_u32(Colors::css_to_u32(Colors::ACTIVE)), Active => Color::LightYellow,
Inactive => Color::from_u32(Colors::css_to_u32(Colors::INACTIVE)), Inactive => Color::White,
} }
} }
} }

View File

@@ -7,8 +7,7 @@ use anyhow::{Context, Result, bail};
use edtui::{ use edtui::{
EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter, EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter,
}; };
use libclide::log::Loggable; use log::{error, trace};
use libclide_macros::Loggable;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use ratatui::layout::{Alignment, Rect}; use ratatui::layout::{Alignment, Rect};
@@ -17,18 +16,19 @@ use ratatui::widgets::{Block, Borders, Padding, Widget};
use std::path::PathBuf; use std::path::PathBuf;
use syntect::parsing::SyntaxSet; use syntect::parsing::SyntaxSet;
#[derive(Loggable)]
pub struct Editor { pub struct Editor {
pub state: EditorState, pub state: EditorState,
pub event_handler: EditorEventHandler, pub event_handler: EditorEventHandler,
pub file_path: Option<PathBuf>, pub file_path: Option<std::path::PathBuf>,
syntax_set: SyntaxSet, syntax_set: SyntaxSet,
pub(crate) component_state: ComponentState, pub(crate) component_state: ComponentState,
} }
impl Editor { impl Editor {
pub const ID: &str = "Editor";
pub fn new(path: &std::path::Path) -> Self { pub fn new(path: &std::path::Path) -> Self {
libclide::trace!("Building {}", <Self as Loggable>::ID); trace!(target:Self::ID, "Building {}", Self::ID);
Editor { Editor {
state: EditorState::default(), state: EditorState::default(),
event_handler: EditorEventHandler::default(), event_handler: EditorEventHandler::default(),
@@ -42,10 +42,10 @@ impl Editor {
} }
pub fn reload_contents(&mut self) -> Result<()> { pub fn reload_contents(&mut self) -> Result<()> {
libclide::trace!("Reloading editor file contents {:?}", self.file_path); trace!(target:Self::ID, "Reloading editor file contents {:?}", self.file_path);
match self.file_path.clone() { match self.file_path.clone() {
None => { None => {
libclide::error!("Failed to reload editor contents with None file_path"); error!(target:Self::ID, "Failed to reload editor contents with None file_path");
bail!("Failed to reload editor contents with None file_path") bail!("Failed to reload editor contents with None file_path")
} }
Some(path) => self.set_contents(&path), Some(path) => self.set_contents(&path),
@@ -53,7 +53,7 @@ impl Editor {
} }
pub fn set_contents(&mut self, path: &std::path::Path) -> Result<()> { pub fn set_contents(&mut self, path: &std::path::Path) -> Result<()> {
libclide::trace!("Setting Editor contents from path {:?}", path); trace!(target:Self::ID, "Setting Editor contents from path {:?}", path);
if let Ok(contents) = std::fs::read_to_string(path) { if let Ok(contents) = std::fs::read_to_string(path) {
let lines: Vec<_> = contents let lines: Vec<_> = contents
.lines() .lines()
@@ -69,10 +69,10 @@ impl Editor {
pub fn save(&self) -> Result<()> { pub fn save(&self) -> Result<()> {
if let Some(path) = &self.file_path { if let Some(path) = &self.file_path {
libclide::trace!("Saving Editor contents {:?}", path); trace!(target:Self::ID, "Saving Editor contents {:?}", path);
return std::fs::write(path, self.state.lines.to_string()).map_err(|e| e.into()); return std::fs::write(path, self.state.lines.to_string()).map_err(|e| e.into());
}; };
libclide::error!("Failed saving Editor contents; file_path was None"); error!(target:Self::ID, "Failed saving Editor contents; file_path was None");
bail!("File not saved. No file path set.") bail!("File not saved. No file path set.")
} }
} }
@@ -115,8 +115,9 @@ impl Component for Editor {
fn handle_event(&mut self, event: Event) -> Result<Action> { fn handle_event(&mut self, event: Event) -> Result<Action> {
if let Some(key_event) = event.as_key_event() { if let Some(key_event) = event.as_key_event() {
// Handle events here that should not be passed on to the vim emulation handler. // Handle events here that should not be passed on to the vim emulation handler.
if let Action::Handled = self.handle_key_events(key_event)? { match self.handle_key_events(key_event)? {
return Ok(Action::Handled); Action::Handled => return Ok(Action::Handled),
_ => {}
} }
} }
self.event_handler.on_event(event, &mut self.state); self.event_handler.on_event(event, &mut self.state);

View File

@@ -5,8 +5,7 @@
use crate::tui::component::{Action, Component, Focus, FocusState}; use crate::tui::component::{Action, Component, Focus, FocusState};
use crate::tui::editor::Editor; use crate::tui::editor::Editor;
use anyhow::{Context, Result, anyhow}; use anyhow::{Context, Result, anyhow};
use libclide::log::Loggable; use log::{error, info, trace, warn};
use libclide_macros::Loggable;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use ratatui::layout::Rect; use ratatui::layout::Rect;
@@ -17,7 +16,6 @@ use std::collections::HashMap;
// Render the tabs with keys as titles // Render the tabs with keys as titles
// Tab keys can be file names. // Tab keys can be file names.
// Render the editor using the key as a reference for lookup // Render the editor using the key as a reference for lookup
#[derive(Loggable)]
pub struct EditorTab { pub struct EditorTab {
pub(crate) editors: HashMap<String, Editor>, pub(crate) editors: HashMap<String, Editor>,
tab_order: Vec<String>, tab_order: Vec<String>,
@@ -25,8 +23,10 @@ pub struct EditorTab {
} }
impl EditorTab { impl EditorTab {
pub const ID: &str = "EditorTab";
pub fn new() -> Self { pub fn new() -> Self {
libclide::trace!("Building {}", <Self as Loggable>::ID); trace!(target:Self::ID, "Building {}", Self::ID);
Self { Self {
editors: HashMap::new(), editors: HashMap::new(),
tab_order: Vec::new(), tab_order: Vec::new(),
@@ -36,11 +36,7 @@ impl EditorTab {
pub fn next_editor(&mut self) { pub fn next_editor(&mut self) {
let next = (self.current_editor + 1) % self.tab_order.len(); let next = (self.current_editor + 1) % self.tab_order.len();
libclide::trace!( trace!(target:Self::ID, "Moving from {} to next editor tab at {}", self.current_editor, next);
"Moving from {} to next editor tab at {}",
self.current_editor,
next
);
self.set_tab_focus(Focus::Active, next); self.set_tab_focus(Focus::Active, next);
self.current_editor = next; self.current_editor = next;
} }
@@ -50,11 +46,7 @@ impl EditorTab {
.current_editor .current_editor
.checked_sub(1) .checked_sub(1)
.unwrap_or(self.tab_order.len() - 1); .unwrap_or(self.tab_order.len() - 1);
libclide::trace!( trace!(target:Self::ID, "Moving from {} to previous editor tab at {}", self.current_editor, prev);
"Moving from {} to previous editor tab at {}",
self.current_editor,
prev
);
self.set_tab_focus(Focus::Active, prev); self.set_tab_focus(Focus::Active, prev);
self.current_editor = prev; self.current_editor = prev;
} }
@@ -63,7 +55,7 @@ impl EditorTab {
match self.tab_order.get(index) { match self.tab_order.get(index) {
None => { None => {
if !self.tab_order.is_empty() { if !self.tab_order.is_empty() {
libclide::error!("Failed to get editor tab key with invalid index {index}"); error!(target:Self::ID, "Failed to get editor tab key with invalid index {index}");
} }
None None
} }
@@ -81,19 +73,16 @@ impl EditorTab {
} }
pub fn set_current_tab_focus(&mut self, focus: Focus) { pub fn set_current_tab_focus(&mut self, focus: Focus) {
libclide::trace!( trace!(target:Self::ID, "Setting current tab {} focus to {:?}", self.current_editor, focus);
"Setting current tab {} focus to {:?}",
self.current_editor,
focus
);
self.set_tab_focus(focus, self.current_editor) self.set_tab_focus(focus, self.current_editor)
} }
pub fn set_tab_focus(&mut self, focus: Focus, index: usize) { pub fn set_tab_focus(&mut self, focus: Focus, index: usize) {
libclide::trace!("Setting tab {} focus to {:?}", index, focus); trace!(target:Self::ID, "Setting tab {} focus to {:?}", index, focus);
if focus == Focus::Active && index != self.current_editor { if focus == Focus::Active && index != self.current_editor {
// If we are setting another tab to active, disable the current one. // If we are setting another tab to active, disable the current one.
libclide::trace!( trace!(
target:Self::ID,
"New tab {} focus set to Active; Setting current tab {} to Inactive", "New tab {} focus set to Active; Setting current tab {} to Inactive",
index, index,
self.current_editor self.current_editor
@@ -102,11 +91,12 @@ impl EditorTab {
} }
match self.get_editor_key(index) { match self.get_editor_key(index) {
None => { None => {
libclide::error!("Failed setting tab focus for invalid key {index}"); error!(target:Self::ID, "Failed setting tab focus for invalid key {index}");
} }
Some(key) => match self.editors.get_mut(&key) { Some(key) => match self.editors.get_mut(&key) {
None => { None => {
libclide::error!( error!(
target:Self::ID,
"Failed to update tab focus at index {} with invalid key: {}", "Failed to update tab focus at index {} with invalid key: {}",
self.current_editor, self.current_editor,
self.tab_order[self.current_editor] self.tab_order[self.current_editor]
@@ -118,12 +108,12 @@ impl EditorTab {
} }
pub fn open_tab(&mut self, path: &std::path::Path) -> Result<()> { pub fn open_tab(&mut self, path: &std::path::Path) -> Result<()> {
libclide::trace!("Opening new EditorTab with path {:?}", path); trace!(target:Self::ID, "Opening new EditorTab with path {:?}", path);
if self if self
.editors .editors
.contains_key(&path.to_string_lossy().to_string()) .contains_key(&path.to_string_lossy().to_string())
{ {
libclide::warn!("EditorTab already opened with this file"); warn!(target:Self::ID, "EditorTab already opened with this file");
return Ok(()); return Ok(());
} }
@@ -148,12 +138,12 @@ impl EditorTab {
.to_owned(); .to_owned();
match self.editors.remove(&key) { match self.editors.remove(&key) {
None => { None => {
libclide::error!("Failed to remove editor tab {key} with invalid index {index}") error!(target:Self::ID, "Failed to remove editor tab {key} with invalid index {index}")
} }
Some(_) => { Some(_) => {
self.prev_editor(); self.prev_editor();
self.tab_order.remove(index); self.tab_order.remove(index);
libclide::info!("Closed editor tab {key} at index {index}") info!(target:Self::ID, "Closed editor tab {key} at index {index}")
} }
} }
Ok(()) Ok(())

View File

@@ -4,33 +4,34 @@
use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState}; use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use libclide::fs::entry_meta::EntryMeta; use log::trace;
use libclide::log::Loggable;
use libclide_macros::Loggable;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind}; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind};
use ratatui::layout::{Alignment, Position, Rect}; use ratatui::layout::{Alignment, Position, Rect};
use ratatui::prelude::Style; use ratatui::prelude::Style;
use ratatui::style::{Color, Modifier}; use ratatui::style::{Color, Modifier};
use ratatui::widgets::{Block, Borders, StatefulWidget, Widget}; use ratatui::widgets::{Block, Borders, StatefulWidget, Widget};
use std::ffi::OsStr;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tui_tree_widget::{Tree, TreeItem, TreeState}; use tui_tree_widget::{Tree, TreeItem, TreeState};
#[derive(Debug, Loggable)] #[derive(Debug)]
pub struct Explorer<'a> { pub struct Explorer<'a> {
root_path: EntryMeta, pub(crate) root_path: PathBuf,
tree_items: TreeItem<'a, String>, tree_items: TreeItem<'a, String>,
tree_state: TreeState<String>, tree_state: TreeState<String>,
pub(crate) component_state: ComponentState, pub(crate) component_state: ComponentState,
} }
impl<'a> Explorer<'a> { impl<'a> Explorer<'a> {
pub const ID: &'static str = "Explorer";
pub fn new(path: &PathBuf) -> Result<Self> { pub fn new(path: &PathBuf) -> Result<Self> {
libclide::trace!("Building {}", <Self as Loggable>::ID); trace!(target:Self::ID, "Building {}", Self::ID);
let explorer = Explorer { let explorer = Explorer {
root_path: EntryMeta::new(path)?, root_path: path.to_owned(),
tree_items: Self::build_tree_from_path(path)?, tree_items: Self::build_tree_from_path(path.to_owned())?,
tree_state: TreeState::default(), tree_state: TreeState::default(),
component_state: ComponentState::default().with_help_text(concat!( component_state: ComponentState::default().with_help_text(concat!(
"(↑/k)/(↓/j): Select item | ←/h: Close folder | →/l: Open folder |", "(↑/k)/(↓/j): Select item | ←/h: Close folder | →/l: Open folder |",
@@ -40,46 +41,46 @@ impl<'a> Explorer<'a> {
Ok(explorer) Ok(explorer)
} }
/// Builds the file tree from a path using recursion. fn build_tree_from_path(path: PathBuf) -> Result<TreeItem<'static, String>> {
/// The identifiers used for the TreeItems are normalized. Symlinks are not resolved.
/// Resolving symlinks would cause collisions on the TreeItem unique identifiers within the set.
fn build_tree_from_path<P: AsRef<Path>>(p: P) -> Result<TreeItem<'static, String>> {
let path = p.as_ref();
let mut children = vec![]; let mut children = vec![];
let path_meta = EntryMeta::new(path)?; let clean_path = fs::canonicalize(path)?;
if let Ok(entries) = fs::read_dir(&path_meta.abs_path) { if let Ok(entries) = fs::read_dir(&clean_path) {
let mut paths = entries let mut paths = entries
.map(|res| res.map(|e| e.path())) .map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, std::io::Error>>() .collect::<Result<Vec<_>, std::io::Error>>()
.context(format!( .context(format!(
"Failed to build vector of paths under directory: {:?}", "Failed to build vector of paths under directory: {:?}",
&path_meta.abs_path clean_path
))?; ))?;
paths.sort(); paths.sort();
for entry_path in paths { for path in paths {
let entry_meta = EntryMeta::new(&entry_path)?; if path.is_dir() {
if entry_meta.is_dir { children.push(Self::build_tree_from_path(path)?);
children.push(Self::build_tree_from_path(&entry_meta.abs_path)?);
} else { } else {
if let Ok(path) = fs::canonicalize(&path) {
let path_str = path.to_string_lossy().to_string();
children.push(TreeItem::new_leaf( children.push(TreeItem::new_leaf(
entry_meta.abs_path.clone(), path_str + uuid::Uuid::new_v4().to_string().as_str(),
format!("{} {}", entry_meta.icon.icon, entry_meta.file_name.as_str()), path.file_name()
.context("Failed to get file name from path.")?
.to_string_lossy()
.to_string(),
)); ));
} }
} }
} }
}
// Note: The first argument is a unique identifier, where no. 2 TreeItems may share the same.
// For a file tree this is fine because we shouldn't list the same object twice.
TreeItem::new( TreeItem::new(
path_meta.abs_path.clone(), clean_path.to_string_lossy().to_string() + uuid::Uuid::new_v4().to_string().as_str(),
format!("{} {}", path_meta.icon.icon, path_meta.file_name.as_str()), clean_path
.file_name()
.context(format!("Failed to get file name from path: {clean_path:?}"))?
.to_string_lossy()
.to_string(),
children, children,
) )
.context(format!( .context(format!("Failed to build tree from path: {clean_path:?}"))
"Failed to build tree from path: {:?}",
path_meta.abs_path
))
} }
pub fn selected(&self) -> Result<String> { pub fn selected(&self) -> Result<String> {
@@ -95,12 +96,16 @@ impl<'a> Explorer<'a> {
impl<'a> Widget for &mut Explorer<'a> { impl<'a> Widget for &mut Explorer<'a> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
if let Ok(tree) = Tree::new(self.tree_items.children()) { if let Ok(tree) = Tree::new(&self.tree_items.children()) {
let file_name = self
.root_path
.file_name()
.unwrap_or_else(|| OsStr::new("Unknown"));
StatefulWidget::render( StatefulWidget::render(
tree.block( tree.block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.title(self.root_path.file_name.clone()) .title(file_name.to_string_lossy())
.border_style(Style::default().fg(self.component_state.get_active_color())) .border_style(Style::default().fg(self.component_state.get_active_color()))
.title_style(Style::default().fg(Color::Green)) .title_style(Style::default().fg(Color::Green))
.title_alignment(Alignment::Center), .title_alignment(Alignment::Center),
@@ -129,22 +134,24 @@ impl<'a> Component for Explorer<'a> {
_ => {} _ => {}
} }
} }
if let Some(mouse_event) = event.as_mouse_event() if let Some(mouse_event) = event.as_mouse_event() {
&& let Action::Handled = self.handle_mouse_events(mouse_event)? match self.handle_mouse_events(mouse_event)? {
{ Action::Handled => return Ok(Action::Handled),
return Ok(Action::Handled); _ => {}
}
} }
Ok(Action::Pass) Ok(Action::Pass)
} }
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> { fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> {
if key.code == KeyCode::Enter if key.code == KeyCode::Enter {
&& let Ok(selected) = self.selected() if let Ok(selected) = self.selected() {
&& Path::new(&selected).is_file() if Path::new(&selected).is_file() {
{
// Open a tab if the selected item is a file.
return Ok(Action::OpenTab); return Ok(Action::OpenTab);
} }
}
// Otherwise fall through and handle Enter in the next match case.
}
let changed = match key.code { let changed = match key.code {
KeyCode::Up | KeyCode::Char('k') => self.tree_state.key_up(), KeyCode::Up | KeyCode::Char('k') => self.tree_state.key_up(),

View File

@@ -3,9 +3,7 @@
// SPDX-License-Identifier: GNU General Public License v3.0 or later // SPDX-License-Identifier: GNU General Public License v3.0 or later
use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState}; use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState};
use libclide::log::Loggable; use log::{LevelFilter, trace};
use libclide_macros::Loggable;
use log::LevelFilter;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent}; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent};
use ratatui::layout::Rect; use ratatui::layout::Rect;
@@ -15,15 +13,16 @@ use tui_logger::{TuiLoggerLevelOutput, TuiLoggerSmartWidget, TuiWidgetEvent, Tui
/// Any log written as info!(target:self.id(), "message") will work with this logger. /// Any log written as info!(target:self.id(), "message") will work with this logger.
/// The logger is bound to info!, debug!, error!, trace! macros within Tui::new(). /// The logger is bound to info!, debug!, error!, trace! macros within Tui::new().
#[derive(Loggable)]
pub struct Logger { pub struct Logger {
state: TuiWidgetState, state: TuiWidgetState,
pub(crate) component_state: ComponentState, pub(crate) component_state: ComponentState,
} }
impl Logger { impl Logger {
pub const ID: &str = "Logger";
pub fn new() -> Self { pub fn new() -> Self {
libclide::trace!("Building {}", <Self as Loggable>::ID); trace!(target:Self::ID, "Building {}", Self::ID);
let state = TuiWidgetState::new(); let state = TuiWidgetState::new();
state.transition(TuiWidgetEvent::HideKey); state.transition(TuiWidgetEvent::HideKey);
Self { Self {

View File

@@ -7,7 +7,7 @@ use crate::tui::menu_bar::MenuBarItemOption::{
About, CloseTab, Exit, Reload, Save, ShowHideExplorer, ShowHideLogger, About, CloseTab, Exit, Reload, Save, ShowHideExplorer, ShowHideLogger,
}; };
use anyhow::Context; use anyhow::Context;
use libclide_macros::Loggable; use log::trace;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{KeyCode, KeyEvent}; use ratatui::crossterm::event::{KeyCode, KeyEvent};
use ratatui::layout::Rect; use ratatui::layout::Rect;
@@ -80,7 +80,6 @@ impl MenuBarItem {
} }
} }
#[derive(Debug, Loggable)]
pub struct MenuBar { pub struct MenuBar {
selected: MenuBarItem, selected: MenuBarItem,
opened: Option<MenuBarItem>, opened: Option<MenuBarItem>,
@@ -89,9 +88,11 @@ pub struct MenuBar {
} }
impl MenuBar { impl MenuBar {
pub const ID: &str = "MenuBar";
const DEFAULT_HELP: &str = "(←/h)/(→/l): Select option | Enter: Choose selection"; const DEFAULT_HELP: &str = "(←/h)/(→/l): Select option | Enter: Choose selection";
pub fn new() -> Self { pub fn new() -> Self {
libclide::trace!("Building"); trace!(target:Self::ID, "Building {}", Self::ID);
Self { Self {
selected: MenuBarItem::File, selected: MenuBarItem::File,
opened: None, opened: None,
@@ -130,7 +131,7 @@ impl MenuBar {
opened: MenuBarItem, opened: MenuBarItem,
) { ) {
let popup_area = Self::rect_under_option(title_bar_anchor, area, 27, 10); let popup_area = Self::rect_under_option(title_bar_anchor, area, 27, 10);
Clear.render(popup_area, buf); Clear::default().render(popup_area, buf);
let options = opened.options().iter().map(|i| ListItem::new(i.id())); let options = opened.options().iter().map(|i| ListItem::new(i.id()));
StatefulWidget::render( StatefulWidget::render(
List::new(options) List::new(options)
@@ -149,14 +150,15 @@ impl MenuBar {
} }
fn rect_under_option(anchor: Rect, area: Rect, width: u16, height: u16) -> Rect { fn rect_under_option(anchor: Rect, area: Rect, width: u16, height: u16) -> Rect {
Rect { let rect = Rect {
x: anchor.x, x: anchor.x,
y: anchor.y + anchor.height, y: anchor.y + anchor.height,
width: width.min(area.width), width: width.min(area.width),
height, height,
} };
// TODO: X offset for item option? It's fine as-is, but it might look nicer. // TODO: X offset for item option? It's fine as-is, but it might look nicer.
// trace!("Building Rect under MenuBar popup {}", rect); // trace!(target:Self::ID, "Building Rect under MenuBar popup {}", rect);
rect
} }
} }