feat: project 3
This commit is contained in:
parent
8f4463782c
commit
c04f363e57
15 changed files with 5425 additions and 15 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -36,6 +36,7 @@
|
|||
*.x86_64
|
||||
*.hex
|
||||
build/
|
||||
target/
|
||||
|
||||
# Debug files
|
||||
*.dSYM/
|
||||
|
|
|
|||
55
flake.lock
generated
55
flake.lock
generated
|
|
@ -1,5 +1,41 @@
|
|||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1763938834,
|
||||
"narHash": "sha256-j8iB0Yr4zAvQLueCZ5abxfk6fnG/SJ5JnGUziETjwfg=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "d9e753122e51cee64eb8d2dddfe11148f339f5a2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1764485195,
|
||||
"narHash": "sha256-VPbuGvsS3rN+CR75xTIG6kk5v1SSQ62DDtz/WImdtfo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "2e31077af24c47ea9057fcb152fa0386444e582a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1756819007,
|
||||
|
|
@ -18,10 +54,29 @@
|
|||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"fenix": "fenix",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"systems": "systems"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1764443484,
|
||||
"narHash": "sha256-0ogtpJuadSpl/Y04GsxtUYHD4GW4ZNV8StJTS2MVvEE=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "66c11a2880f6a929e3ba811aa6102c4d9fe2f77a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
|
|
|
|||
11
flake.nix
11
flake.nix
|
|
@ -4,9 +4,13 @@
|
|||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
systems.url = "github:nix-systems/default";
|
||||
|
||||
crane.url = "github:ipetkov/crane";
|
||||
fenix.url = "github:nix-community/fenix";
|
||||
fenix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, systems, ... }:
|
||||
outputs = inputs@{ self, nixpkgs, systems, crane, fenix, ... }:
|
||||
let
|
||||
subdirs = [
|
||||
"assignment1"
|
||||
|
|
@ -14,6 +18,7 @@
|
|||
"assignment3"
|
||||
"project1"
|
||||
"project2"
|
||||
"project3"
|
||||
];
|
||||
|
||||
eachSystem = nixpkgs.lib.genAttrs (import systems);
|
||||
|
|
@ -23,7 +28,7 @@
|
|||
in pkgs.lib.mergeAttrsList
|
||||
(builtins.map (d:
|
||||
builtins.mapAttrs
|
||||
(_: v: pkgs.callPackage v {})
|
||||
(_: v: pkgs.callPackage v { inherit inputs; })
|
||||
(import ./${d}/${file}))
|
||||
subdirs));
|
||||
in {
|
||||
|
|
@ -33,7 +38,7 @@
|
|||
devShells = eachSystem (system:
|
||||
let pkgs = nixpkgs.legacyPackages.${system};
|
||||
in {
|
||||
default = pkgs.callPackage ./shell.nix {};
|
||||
default = import ./shell.nix { inherit pkgs crane fenix; };
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@
|
|||
\title{CS3502 Project 2: CPU Scheduling Simulator}
|
||||
\author{Kiana Sheibani}
|
||||
|
||||
\definecolor{lgray}{gray}{0.9}
|
||||
|
||||
\begin{document}
|
||||
\maketitle
|
||||
\newpage
|
||||
|
|
|
|||
4158
project3/Cargo.lock
generated
Normal file
4158
project3/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
8
project3/Cargo.toml
Normal file
8
project3/Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "file_manager"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
winit = "0.30.12"
|
||||
xilem = "0.4"
|
||||
37
project3/default.nix
Normal file
37
project3/default.nix
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
rec {
|
||||
project3 = file-manager;
|
||||
file-manager = { pkgs, inputs }:
|
||||
let
|
||||
inherit (inputs) crane fenix;
|
||||
toolchain = fenix.packages.${pkgs.system}.complete.withComponents [
|
||||
"rustc" "rust-std" "cargo" "rust-docs" "rustfmt" "clippy" "rust-src"
|
||||
];
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain toolchain;
|
||||
|
||||
commonArgs = {
|
||||
src = craneLib.cleanCargoSource ./.;
|
||||
strictDeps = true;
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
pkg-config
|
||||
fontconfig
|
||||
wayland
|
||||
wayland-protocols
|
||||
wayland-scanner
|
||||
vulkan-loader
|
||||
libxkbcommon
|
||||
xorg.libxcb
|
||||
xorg.libX11
|
||||
xorg.libXcursor
|
||||
xorg.libXi
|
||||
xorg.libXrandr
|
||||
xorg.libXxf86vm
|
||||
];
|
||||
|
||||
passthru.commonArgs = commonArgs;
|
||||
};
|
||||
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
crate = craneLib.buildPackage (commonArgs // { inherit cargoArtifacts; });
|
||||
in crate;
|
||||
}
|
||||
BIN
project3/report/assets/dialog.png
Normal file
BIN
project3/report/assets/dialog.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
BIN
project3/report/assets/gui.png
Normal file
BIN
project3/report/assets/gui.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 307 KiB |
237
project3/report/report.tex
Normal file
237
project3/report/report.tex
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
\documentclass{article}
|
||||
|
||||
\usepackage{graphicx}
|
||||
\usepackage{xcolor}
|
||||
\usepackage{minted}
|
||||
\usepackage{hyperref}
|
||||
|
||||
\title{CS3502 Project 3: File Manager}
|
||||
\author{Kiana Sheibani}
|
||||
|
||||
\begin{document}
|
||||
|
||||
\maketitle
|
||||
\newpage
|
||||
|
||||
\section{Introduction}
|
||||
|
||||
Circumstance compels us to program a GUI file manager from scratch, as it so
|
||||
often does.
|
||||
|
||||
\subsection{Objective}
|
||||
|
||||
A file manager is a tool for reading and manipulating a file-system, typically
|
||||
the primary file-system of the machine. It allows the user to perform various
|
||||
operations on the files and directories (\emph{entries} in aggregate) of the
|
||||
file-system, such as to:
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Create} new entries;
|
||||
\item \textbf{Read} and \textbf{Update} the contents of entries;
|
||||
\item \textbf{Delete} entries.
|
||||
\end{itemize}
|
||||
|
||||
Commonly, file managers are designed to allow this manipulation by having the
|
||||
user traverse the filesystem one directory at a time, moving from a single
|
||||
working directory to its sub- or parent directories.
|
||||
|
||||
\subsection{Resources}
|
||||
|
||||
Implementing a file manager is not a trivial task. Properly navigating a
|
||||
file-system requires quite a bit of engineering around complex systems,
|
||||
unintuitive edge cases and strange errors, and some languages and frameworks are
|
||||
better equipped to handle this than others.
|
||||
|
||||
After considering this fact, I decided on \href{https://rust-lang.org/}{Rust} as
|
||||
my language of choice. Not only do I have significant prior experience with it,
|
||||
but its low-level nature and near-slavish dedication to correctness make it
|
||||
ideal for working with file-systems.
|
||||
|
||||
Once I had my language picked, I searched for GUI libraries I could use for the
|
||||
front-end. It was not long before I found
|
||||
\href{https://crates.io/crates/xilem/}{Xilem}, an experimental cross-platform UI
|
||||
toolkit. It was still somewhat unstable, but I decided it would be perfectly
|
||||
fine for my purposes.%
|
||||
\footnote{This assumption turned out to be rather short-sighted, but I made it
|
||||
work as best I could.}
|
||||
|
||||
\section{Design and Architecture}
|
||||
|
||||
\subsection{Front-End Design}
|
||||
|
||||
\begin{figure}[tbh]
|
||||
\includegraphics[width=\linewidth]{assets/gui.png}
|
||||
\caption{The UI of the file manager.}
|
||||
\label{fig:gui}
|
||||
\end{figure}
|
||||
|
||||
The UI design of the file manager is somewhat inspired by
|
||||
\href{https://github.com/ranger/ranger}{{\texttt{ranger}}}, though heavily
|
||||
simplified. The current directory path is displayed at the top, and its entries
|
||||
are listed in the main panel below it. When a file is opened, its contents
|
||||
appear on a panel to the right, and remain there until another file is opened.
|
||||
|
||||
The file-system can be navigated by either double-clicking on a subdirectory, or
|
||||
selecting it and then clicking the \emph{Open} button at the bottom. To move up
|
||||
the directory tree, a component of the topmost path display can be clicked on.
|
||||
There are also several other action buttons along the bottom; these operate on
|
||||
the currently selected entry (aside from the \emph{New File} and \emph{New Dir}
|
||||
actions) and using them pops up a dialog box to confirm the action. The dialog
|
||||
box popup is also used to display errors whenever they occur.
|
||||
|
||||
\begin{figure}[tbh]
|
||||
\includegraphics[width=\linewidth]{assets/dialog.png}
|
||||
\caption{The file manager showing a file renaming dialog.}
|
||||
\label{fig:dialog}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Program Architecture}
|
||||
|
||||
The structure of the program can be conceptually divided into two halves: the
|
||||
file-system management subroutines, and the GUI logic.
|
||||
|
||||
\subsubsection{File-System Management}
|
||||
|
||||
Internally, an entry in the current directory is stored as an \texttt{EntryData}
|
||||
object (Listing~\ref{listing:EntryData}), which holds the entry's name and other
|
||||
metadata. The basic file-system operations, such as renaming and deleting
|
||||
entries, all operate on this data structure.
|
||||
|
||||
\begin{listing}[!ht]
|
||||
\inputminted[
|
||||
firstline=7, lastline=17,
|
||||
fontsize=\footnotesize,
|
||||
linenos,
|
||||
frame=lines,
|
||||
framesep=2mm,
|
||||
baselinestretch=1.1,
|
||||
]{rust}{../src/files.rs}
|
||||
\vspace*{-\baselineskip}
|
||||
\caption{\texttt{files.rs} --- Definition of \texttt{EntryData}}
|
||||
\label{listing:EntryData}
|
||||
\end{listing}
|
||||
|
||||
Reading and editing a file, on the other hand, requires a different structure to
|
||||
store and manipulate the file handle. This is defined as \texttt{FileData}
|
||||
(Listing~\ref{listing:FileData}). When a file is opened, the file handle is
|
||||
retained for the entire duration the file is viewed, only being closed when the
|
||||
file is exited. The \texttt{files} module of the program implements an
|
||||
\texttt{open} function to create this data structure and a \texttt{save}
|
||||
function to write to the file handle that it holds.
|
||||
|
||||
\begin{listing}[!ht]
|
||||
\inputminted[
|
||||
firstline=72, lastline=85,
|
||||
fontsize=\footnotesize,
|
||||
linenos,
|
||||
frame=lines,
|
||||
framesep=2mm,
|
||||
baselinestretch=1.1,
|
||||
]{rust}{../src/files.rs}
|
||||
\vspace*{-\baselineskip}
|
||||
\caption{\texttt{files.rs} --- Definition of \texttt{FileData}}
|
||||
\label{listing:FileData}
|
||||
\end{listing}
|
||||
|
||||
\subsubsection{GUI \& Error Handling}
|
||||
|
||||
Xilem is a reactive GUI framework based on composable widgets, similar to Qt.
|
||||
The system is based around a data structure representing the app's state; in the
|
||||
case of this program, the \texttt{AppState} type includes the current directory,
|
||||
selected entry, and state for any open panels, such as the \texttt{FileData} for
|
||||
the open file.
|
||||
|
||||
\texttt{AppState} also includes the \texttt{DialogState}, which is an enum with
|
||||
one value per type of dialog that can be opened: \texttt{NewFile},
|
||||
\texttt{NewDir}, \texttt{Rename}, \texttt{Delete}, and \texttt{Error}, where the
|
||||
last value also includes an error message to display. Determining when to show
|
||||
these errors is simple: thanks to Rust's aforementioned obsession with
|
||||
correctness, every file-system routine it provides that could ever conceivably
|
||||
result in any sort of error wraps its return type in a \texttt{std::io::Result}
|
||||
data structure, which forces the programmer to handle the error case before the
|
||||
actual output can be retrieved. This is why I chose Rust for this project, and
|
||||
it led to very simple and easy-to-understand file management code.
|
||||
|
||||
\section{Testing \& Edge Cases}
|
||||
|
||||
In order to ensure the robustness of my work, I regularly tested it manually to
|
||||
make sure that it functioned properly and handled any of the weird edge cases
|
||||
file-system code is prone to. Some of these issues I was able to fully
|
||||
eliminate, while others were a bit more stubborn.
|
||||
|
||||
\subsection{File-System Race Conditions}
|
||||
|
||||
The file manager is only one of many processes that can access the file-system,
|
||||
which can lead to issues if two processes are trying to modify the same
|
||||
directory at the same time. For example, if another process deletes a file in
|
||||
the current directory of the file manager, an orphan entry may be left over that
|
||||
no longer points to an existing file. While the file manager will not
|
||||
automatically detect that this has occurred, it will throw an error if the user
|
||||
tries to access an entry that no longer exists.
|
||||
|
||||
\subsection{Uncommon Entry Types}
|
||||
|
||||
So far, the only two entry types mentioned have been files and subdirectories,
|
||||
the two most common types. There are others, however; symbolic links, or
|
||||
``symlinks'', and more esoteric file types like named pipes, device files, etc.
|
||||
Symlinks are of particular concern, as they violate the file-system's otherwise
|
||||
perfect tree structure.
|
||||
|
||||
To avoid hard-to-debug issues down the line, I decided on a policy for handling
|
||||
symlinks from the start:
|
||||
|
||||
\begin{itemize}
|
||||
\item In the main panel, symlinks are not followed and are displayed
|
||||
distinctly from the entries that they point to, including their
|
||||
metadata. This is done for performance reasons.
|
||||
\item Opening a symlink follows it to its target, with the caveat that
|
||||
if a symlink is followed into a subdirectory, the displayed path is
|
||||
canonicalized. This simplifies the navigation.
|
||||
\end{itemize}
|
||||
|
||||
All other entry types simply throw an error when the user tries to open them, as
|
||||
do any files that do not contain UTF-8 text.
|
||||
|
||||
\subsection{Unusual Entry Names}
|
||||
|
||||
While most entry names are simple Latin letters and numbers, depending on the
|
||||
operating system there are many other possibilities. Unix-based systems
|
||||
typically have UTF-8 entry names, though they technically allow any string of
|
||||
bytes except for \texttt{0x00} (NUL) or \texttt{0x2F} (/). Windows, on the other
|
||||
hand, has historically used UTF-16 for its entry names, as it predated the wide
|
||||
adoption of the superior UTF-8.
|
||||
|
||||
To allow cross-platform programs to navigate this confusion, the Rust standard
|
||||
library provides the \texttt{OsString} type, which represents a string in the
|
||||
format the target OS prefers. Due to this and some careful programming, the file
|
||||
manager will never crash due to unsupported entry names (at least on Linux; I
|
||||
have not personally tested Windows). Unfortunately, while non-Unicode entry
|
||||
names can be read, they cannot be entered into dialog text fields. This is an
|
||||
inherent limitation of Xilem that is unlikely to change.
|
||||
|
||||
\subsection{Large File Sizes}
|
||||
|
||||
This is the largest issue that still remains in the file manager. Due to the
|
||||
fact that a full buffer of an open file's contents must be maintained, the file
|
||||
editor is exceedingly slow with files over a few kilobytes, and trying to open
|
||||
anything larger than a megabyte has a chance of causing the application to
|
||||
crash. Ideally, the program would use some sort of line buffering scheme to
|
||||
avoid having to update the file all at once, but I was not able to implement
|
||||
anything that worked well.
|
||||
|
||||
\section{Conclusion}
|
||||
|
||||
After roughly a week's effort, a basic file manager has been achieved. Though
|
||||
the visual aesthetic and the file editing feature may be improved, this
|
||||
serves as a strong foundation for a well-programmed application.
|
||||
|
||||
\subsection{Learning Outcomes}
|
||||
|
||||
While I most likely started from a more knowledgeable place on this subject than
|
||||
most in this class, I did learn some new things from this process. I was not
|
||||
nearly as knowledgeable on the minutiae of OS file-systems when I started this
|
||||
project as I am now, nor was I as experienced in writing GUI applications that
|
||||
properly interface with that OS minutia. At the end of it all, I am glad that
|
||||
something of some use came out of this.
|
||||
|
||||
\end{document}
|
||||
725
project3/src/app.rs
Normal file
725
project3/src/app.rs
Normal file
|
|
@ -0,0 +1,725 @@
|
|||
//! GUI code for the primary app logic.
|
||||
use std::ffi::OsString;
|
||||
use std::fs::File;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use xilem::{
|
||||
masonry::{
|
||||
parley::{FontFamily, FontStack, GenericFamily},
|
||||
properties::types::AsUnit,
|
||||
},
|
||||
palette::css::*,
|
||||
style::*,
|
||||
view::*,
|
||||
*,
|
||||
};
|
||||
|
||||
use crate::files::{self, EntryData, FileData};
|
||||
|
||||
// -- CONSTANTS
|
||||
|
||||
// Colors
|
||||
const BG: Color = Color::from_rgb8(24, 25, 30);
|
||||
const BG_ALT: Color = Color::from_rgb8(48, 52, 64);
|
||||
const BG_ALT2: Color = Color::from_rgb8(90, 100, 125);
|
||||
const FG: Color = Color::from_rgb8(205, 215, 255);
|
||||
const BG_SEL: Color = Color::from_rgb8(32, 100, 232);
|
||||
|
||||
// Fonts
|
||||
const TEXT_SIZE: f32 = 14.;
|
||||
const FONT: FontStack<'static> = FontStack::Single(FontFamily::Generic(GenericFamily::SansSerif));
|
||||
const FONT_MONO: FontStack<'static> =
|
||||
FontStack::Single(FontFamily::Generic(GenericFamily::Monospace));
|
||||
|
||||
// -- APP STATE
|
||||
|
||||
/// The app's global state.
|
||||
pub struct AppState {
|
||||
// Directory Viewer
|
||||
/// The directory currently being viewed.
|
||||
///
|
||||
/// This will always be an absolute and canonicalized path.
|
||||
dir: PathBuf,
|
||||
/// A cache of the entries for the current directory.
|
||||
///
|
||||
/// This may be `None` if the directory hasn't been read yet,
|
||||
/// or if an error occurred while reading.
|
||||
entries: Option<Vec<EntryData>>,
|
||||
/// The currently selected directory entry.
|
||||
selected: Option<usize>,
|
||||
|
||||
// File Viewer
|
||||
/// The data associated with the opened file.
|
||||
file_viewer: Option<FileData>,
|
||||
|
||||
// Dialog
|
||||
/// The current type/state of the dialog, if one is open.
|
||||
dialog: Option<DialogState>,
|
||||
/// The buffer for the dialog's text field.
|
||||
///
|
||||
/// This is stored separately from the dialog state for convenience.
|
||||
dialog_text_field: String,
|
||||
}
|
||||
|
||||
/// The current state of an open dialog.
|
||||
///
|
||||
/// This primarily determines what type of dialog is opened.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
enum DialogState {
|
||||
/// An error dialog.
|
||||
Error {
|
||||
/// The error message to display.
|
||||
message: String,
|
||||
},
|
||||
/// A dialog for creating a new file.
|
||||
NewFile,
|
||||
/// A dialog for creating a new directory.
|
||||
NewDir,
|
||||
/// A dialog for renaming a file or directory.
|
||||
Rename,
|
||||
/// A dialog for deleting a file or directory.
|
||||
Delete,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
/// Creates a new app state viewing the given directory.
|
||||
pub fn new(dir: PathBuf) -> Self {
|
||||
Self {
|
||||
dir,
|
||||
entries: None,
|
||||
selected: None,
|
||||
dialog: None,
|
||||
dialog_text_field: String::new(),
|
||||
file_viewer: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the selected directory entry.
|
||||
fn selected_entry(&self) -> Option<&EntryData> {
|
||||
Some(&self.entries.as_ref()?[self.selected?])
|
||||
}
|
||||
|
||||
// -- ACTIONS
|
||||
|
||||
/// Change the current directory.
|
||||
///
|
||||
/// The path `dir` must be absolute and canonicalized.
|
||||
fn change_dir(&mut self, dir: &Path) -> std::io::Result<()> {
|
||||
let old_entries = self.entries.take();
|
||||
self.entries = Some(files::get_entries(dir)?);
|
||||
std::env::set_current_dir(dir).inspect_err(|_| {
|
||||
// Revert entry cache if chdir fails
|
||||
self.entries = old_entries;
|
||||
})?;
|
||||
self.dir = dir.to_owned();
|
||||
self.selected = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Populate the entry cache for the current directory.
|
||||
fn populate_entries(&mut self) -> std::io::Result<()> {
|
||||
self.change_dir(&self.dir.clone())
|
||||
}
|
||||
|
||||
/// Select the entry at `index`.
|
||||
fn select(&mut self, index: usize) {
|
||||
self.selected = Some(index);
|
||||
}
|
||||
|
||||
/// Open the selected entry.
|
||||
///
|
||||
///
|
||||
/// If the selected entry is a directory, view it.
|
||||
/// If it is a file, open it in the file viewer.
|
||||
/// If it is a symlink, follow it.
|
||||
/// Otherwise, display an error.
|
||||
fn open(&mut self) -> std::io::Result<()> {
|
||||
if let Some(data) = self.selected_entry() {
|
||||
let canon = Path::new(&data.name).canonicalize()?;
|
||||
if canon.is_dir() {
|
||||
self.change_dir(&canon)?;
|
||||
} else if canon.is_file() {
|
||||
self.file_viewer = Some(files::open(&data.name)?);
|
||||
} else {
|
||||
self.error(format!(
|
||||
"Error opening {:?}:\nUnsupported file type",
|
||||
data.name
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a new file.
|
||||
fn new_file(&mut self) -> std::io::Result<()> {
|
||||
File::create_new(&self.dialog_text_field)?;
|
||||
self.populate_entries()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a new directory.
|
||||
fn new_dir(&mut self) -> std::io::Result<()> {
|
||||
std::fs::create_dir_all(&self.dialog_text_field)?;
|
||||
self.populate_entries()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create the currently selected entry.
|
||||
fn rename(&mut self) -> std::io::Result<()> {
|
||||
if let Some(entry) = self.selected_entry() {
|
||||
files::rename_entry(&entry, &self.dialog_text_field)?;
|
||||
self.populate_entries()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete the currently selected entry.
|
||||
fn delete(&mut self) -> std::io::Result<()> {
|
||||
if let Some(entry) = self.selected_entry() {
|
||||
files::delete_entry(&entry)?;
|
||||
self.populate_entries()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Display a dialog with the given state.
|
||||
fn dialog(&mut self, dialog_state: DialogState) {
|
||||
self.dialog = Some(dialog_state);
|
||||
self.dialog_text_field = String::new();
|
||||
}
|
||||
|
||||
/// Display an error dialog with the given message.
|
||||
fn error(&mut self, message: String) {
|
||||
self.dialog = Some(DialogState::Error { message });
|
||||
}
|
||||
}
|
||||
|
||||
// -- VIEWS
|
||||
|
||||
macro_rules! custom_label {
|
||||
($text:expr $(, $align:ident)? $(,)?) => {
|
||||
label($text)
|
||||
.text_size(TEXT_SIZE)
|
||||
$(.text_alignment(xilem::TextAlign::$align))?
|
||||
.font(FONT)
|
||||
.color(FG)
|
||||
.disabled_color(BG_ALT)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! custom_label_mono {
|
||||
($text:expr $(, $align:ident)? $(,)?) => {
|
||||
label($text)
|
||||
.text_size(TEXT_SIZE)
|
||||
$(.text_alignment(xilem::TextAlign::$align))?
|
||||
.font(FONT_MONO)
|
||||
.color(FG)
|
||||
.disabled_color(BG_ALT)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! custom_button {
|
||||
($text:expr, $callback:expr $(, $disabled:expr)? $(,)?) => {
|
||||
button(custom_label!($text), $callback)$(.disabled($disabled))?
|
||||
.background_color(BG_ALT)
|
||||
.active_background_color(BG_ALT2)
|
||||
.border_color(BG_ALT2)
|
||||
.hovered_border_color(FG)
|
||||
.disabled_background_color(BG)
|
||||
};
|
||||
}
|
||||
|
||||
/// A view to display the currently viewed directory path.
|
||||
fn path_display(state: &mut AppState) -> impl WidgetView<AppState> + use<> {
|
||||
// Iterate over ancestors of current dir and generate one button for each
|
||||
let mut buttons = (&state.dir)
|
||||
.ancestors()
|
||||
.map(|p| {
|
||||
let p_buf = p.to_owned();
|
||||
custom_button!(
|
||||
if let Some(name) = p.file_name() {
|
||||
// Display filename component of each ancestor
|
||||
name.display().to_string()
|
||||
} else {
|
||||
// `p` is always absolute and canonicalized, so if it
|
||||
// doesn't have a filename component it must be root
|
||||
p.display().to_string()
|
||||
},
|
||||
move |state: &mut AppState| {
|
||||
if state.dialog.is_some() {
|
||||
return;
|
||||
}
|
||||
// Change current directory on button press
|
||||
state.change_dir(&p_buf).unwrap_or_else(|err| {
|
||||
state.error(format!(
|
||||
"Error reading directory {:?}:\n{}",
|
||||
p_buf.display(),
|
||||
err
|
||||
))
|
||||
})
|
||||
},
|
||||
)
|
||||
.background_color(BG)
|
||||
.active_background_color(BG_ALT)
|
||||
.border_color(BG_ALT)
|
||||
.hovered_border_color(BG_ALT2)
|
||||
.corner_radius(2.)
|
||||
.padding(Padding::from_vh(4., 6.))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// The `ancestors` iterator yields paths ascending towards root, so
|
||||
// the button order must be reversed
|
||||
buttons.reverse();
|
||||
|
||||
flex_row(buttons)
|
||||
.gap(0.px())
|
||||
.must_fill_major_axis(true)
|
||||
.main_axis_alignment(MainAxisAlignment::Start)
|
||||
.cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.padding(Padding::from_vh(10., 15.))
|
||||
.border(BG_ALT, 2.)
|
||||
}
|
||||
|
||||
/// A view to display a directory entry.
|
||||
fn entry_view(state: &mut AppState, entry: EntryData) -> impl WidgetView<AppState> + use<> {
|
||||
let selected = state.selected == Some(entry.index);
|
||||
|
||||
button(
|
||||
flex_row(custom_label!(entry.name.display().to_string())).must_fill_major_axis(true),
|
||||
move |state: &mut AppState| {
|
||||
if state.dialog.is_some() {
|
||||
return;
|
||||
}
|
||||
if selected {
|
||||
// Open on click if already selected (double click)
|
||||
state.open().unwrap_or_else(|err| {
|
||||
state.error(format!("Error opening entry {:?}:\n{}", entry.name, err))
|
||||
});
|
||||
} else {
|
||||
// Select on click
|
||||
state.select(entry.index);
|
||||
}
|
||||
},
|
||||
)
|
||||
.corner_radius(0.)
|
||||
.border_color(TRANSPARENT)
|
||||
.hovered_border_color(TRANSPARENT)
|
||||
.background_color(if selected { BG_SEL } else { TRANSPARENT })
|
||||
.active_background_color(if selected { BG_SEL } else { BG_ALT })
|
||||
.padding(Padding::vertical(4.))
|
||||
}
|
||||
|
||||
/// A view to display the entries of the current directory.
|
||||
fn dir_view(state: &mut AppState) -> impl WidgetView<AppState> + use<> {
|
||||
// Populate entries if they are unavailable
|
||||
if state.entries.is_none() {
|
||||
state.populate_entries().unwrap_or_else(|err| {
|
||||
state.error(format!("Error reading directory {:?}: {}", state.dir, err));
|
||||
});
|
||||
}
|
||||
|
||||
virtual_scroll(
|
||||
0..(state.entries.as_ref().unwrap().len() as i64),
|
||||
|state: &mut AppState, i| {
|
||||
let i = i as usize;
|
||||
let entries = state.entries.as_ref().unwrap();
|
||||
if i < entries.len() {
|
||||
entry_view(state, entries[i].clone())
|
||||
} else {
|
||||
// HACK: For some reason we need to have a placeholder here
|
||||
// in case Xilem calls the callback with an out-of-bounds index
|
||||
entry_view(
|
||||
state,
|
||||
EntryData {
|
||||
index: i,
|
||||
name: OsString::from(""),
|
||||
metadata: state.dir.metadata().unwrap(),
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn entry_metadata(state: &mut AppState) -> Option<impl WidgetView<AppState> + use<>> {
|
||||
fn display_size(size: u64) -> String {
|
||||
const UNITS: &[&'static str] = &["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
|
||||
const KILO: f64 = (1 << 10) as f64;
|
||||
|
||||
let mut unit = 0;
|
||||
let mut size_f = size as f64;
|
||||
while size_f >= KILO && unit < UNITS.len() - 1 {
|
||||
unit += 1;
|
||||
size_f /= KILO;
|
||||
}
|
||||
|
||||
if unit == 0 {
|
||||
format!(" {}B", size)
|
||||
} else {
|
||||
format!(" {:.2}{}", size_f, UNITS[unit])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn display_permissions(mode: u32) -> String {
|
||||
"rwxrwxrwx"
|
||||
.chars()
|
||||
.enumerate()
|
||||
.map(|(i, c)| if mode >> (8 - i) & 1 == 1 { c } else { '-' })
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn display_file_type(entry: &EntryData) -> String {
|
||||
String::from(if entry.metadata.is_file() {
|
||||
"File"
|
||||
} else if entry.metadata.is_dir() {
|
||||
"Directory"
|
||||
} else if entry.metadata.is_symlink() {
|
||||
"Symlink"
|
||||
} else {
|
||||
"Other"
|
||||
})
|
||||
}
|
||||
|
||||
if let Some(entry) = state.selected_entry() {
|
||||
// Unix - Display permission bits
|
||||
#[cfg(unix)]
|
||||
let permissions = display_permissions(entry.metadata.permissions().mode());
|
||||
|
||||
// Non-Unix - Read-only flag
|
||||
#[cfg(not(unix))]
|
||||
let permissions = if entry.metadata.read_only() {
|
||||
"Read-Only"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let size = if entry.metadata.is_file() {
|
||||
&display_size(entry.metadata.len())
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let file_type = display_file_type(entry);
|
||||
|
||||
Some(custom_label_mono!(
|
||||
&format!(
|
||||
"{}{} - {} ({})",
|
||||
permissions,
|
||||
size,
|
||||
entry.name.display(),
|
||||
file_type
|
||||
)[..]
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A view for the buttons to trigger directory entry operations.
|
||||
fn entry_ops(state: &mut AppState) -> impl WidgetView<AppState> + use<> {
|
||||
flex_row((
|
||||
custom_button!("New File", |state: &mut AppState| {
|
||||
if state.dialog.is_some() {
|
||||
return;
|
||||
}
|
||||
state.dialog(DialogState::NewFile);
|
||||
}),
|
||||
custom_button!("New Dir", |state: &mut AppState| {
|
||||
if state.dialog.is_some() {
|
||||
return;
|
||||
}
|
||||
state.dialog(DialogState::NewDir);
|
||||
}),
|
||||
custom_button!(
|
||||
"Open",
|
||||
|state: &mut AppState| {
|
||||
if state.dialog.is_some() {
|
||||
return;
|
||||
}
|
||||
state.open().unwrap_or_else(|err| {
|
||||
state.error(format!(
|
||||
"Error opening entry {:?}:\n{}",
|
||||
state.selected_entry().unwrap().name,
|
||||
err
|
||||
));
|
||||
});
|
||||
},
|
||||
state.selected.is_none(),
|
||||
),
|
||||
custom_button!(
|
||||
"Rename",
|
||||
|state: &mut AppState| {
|
||||
if state.dialog.is_some() {
|
||||
return;
|
||||
}
|
||||
state.dialog(DialogState::Rename);
|
||||
},
|
||||
state.selected.is_none(),
|
||||
),
|
||||
custom_button!(
|
||||
"Delete",
|
||||
|state: &mut AppState| {
|
||||
if state.dialog.is_some() {
|
||||
return;
|
||||
}
|
||||
state.dialog(DialogState::Delete);
|
||||
},
|
||||
state.selected.is_none(),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
/// A view for the main file viewer.
|
||||
fn file_view() -> impl WidgetView<AppState> + use<> {
|
||||
virtual_scroll(0..1, |state: &mut AppState, _| {
|
||||
let file_state = state.file_viewer.as_ref().unwrap();
|
||||
text_input(
|
||||
file_state.buffer.clone(),
|
||||
// Run on text field changed
|
||||
|state: &mut AppState, input: String| {
|
||||
let file_state = state.file_viewer.as_mut().unwrap();
|
||||
// Update file buffer
|
||||
file_state.buffer = input;
|
||||
// Set modified flag
|
||||
file_state.modified = true;
|
||||
},
|
||||
)
|
||||
// Disable interaction if file is readonly
|
||||
.disabled(file_state.readonly || state.dialog.is_some())
|
||||
// Allow newline insertion
|
||||
.insert_newline(InsertNewline::OnEnter)
|
||||
.text_color(FG)
|
||||
.disabled_text_color(FG)
|
||||
.border_width(0.)
|
||||
})
|
||||
}
|
||||
|
||||
/// A view for the buttons to save and close the file viewer.
|
||||
fn file_ops(state: &mut AppState) -> impl WidgetView<AppState> + use<> {
|
||||
let file_state = state.file_viewer.as_ref().unwrap();
|
||||
let file_name = file_state.name.display().to_string();
|
||||
flex_row((
|
||||
custom_label_mono!(if file_state.readonly {
|
||||
format!("{} (Read-Only)", file_name)
|
||||
} else if file_state.modified {
|
||||
format!("{} (*)", file_name)
|
||||
} else {
|
||||
file_name
|
||||
}),
|
||||
FlexSpacer::Flex(1.0),
|
||||
custom_button!(
|
||||
"Save",
|
||||
|state: &mut AppState| {
|
||||
if state.dialog.is_some() {
|
||||
return;
|
||||
}
|
||||
files::save(state.file_viewer.as_mut().unwrap()).unwrap_or_else(|err| {
|
||||
state.error(format!(
|
||||
"Error saving file {:?}:\n{}",
|
||||
state.file_viewer.as_ref().unwrap().name,
|
||||
err
|
||||
));
|
||||
});
|
||||
},
|
||||
file_state.readonly,
|
||||
),
|
||||
custom_button!("Close", |state: &mut AppState| {
|
||||
if state.dialog.is_some() {
|
||||
return;
|
||||
}
|
||||
state.file_viewer = None;
|
||||
}),
|
||||
))
|
||||
.padding(Padding::all(8.))
|
||||
}
|
||||
|
||||
/// A view for the dialog popup.
|
||||
fn dialog(state: &mut AppState, dialog_state: DialogState) -> impl WidgetView<AppState> + use<> {
|
||||
// Error Dialog
|
||||
let error = if let DialogState::Error { message } = &dialog_state {
|
||||
Some((
|
||||
custom_label!(&message[..], Center),
|
||||
custom_button!("OK", |state: &mut AppState| {
|
||||
state.dialog = None;
|
||||
}),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// New File Dialog
|
||||
let new_file = if &dialog_state == &DialogState::NewFile {
|
||||
let action = |state: &mut AppState| {
|
||||
state.dialog = None;
|
||||
if !state.dialog_text_field.is_empty() {
|
||||
state.new_file().unwrap_or_else(|err| {
|
||||
state.error(format!(
|
||||
"Error creating file {:?}:\n{}",
|
||||
state.dialog_text_field, err
|
||||
));
|
||||
});
|
||||
}
|
||||
};
|
||||
Some((
|
||||
custom_label!("Name of new file:"),
|
||||
text_input(
|
||||
state.dialog_text_field.clone(),
|
||||
|state: &mut AppState, input: String| state.dialog_text_field = input,
|
||||
)
|
||||
.on_enter(move |state, _| action(state)),
|
||||
flex_row((
|
||||
custom_button!("Create", action, state.dialog_text_field.is_empty()),
|
||||
custom_button!("Cancel", |state: &mut AppState| {
|
||||
state.dialog = None;
|
||||
}),
|
||||
)),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let new_dir = if &dialog_state == &DialogState::NewDir {
|
||||
let action = |state: &mut AppState| {
|
||||
state.dialog = None;
|
||||
if !state.dialog_text_field.is_empty() {
|
||||
state.new_dir().unwrap_or_else(|err| {
|
||||
state.error(format!(
|
||||
"Error creating directory {:?}:\n{}",
|
||||
state.dialog_text_field, err
|
||||
));
|
||||
});
|
||||
}
|
||||
};
|
||||
Some((
|
||||
custom_label!("Name of new directory:"),
|
||||
text_input(
|
||||
state.dialog_text_field.clone(),
|
||||
|state: &mut AppState, input: String| state.dialog_text_field = input,
|
||||
)
|
||||
.on_enter(move |state, _| action(state)),
|
||||
flex_row((
|
||||
custom_button!("Create", action, state.dialog_text_field.is_empty()),
|
||||
custom_button!("Cancel", |state: &mut AppState| {
|
||||
state.dialog = None;
|
||||
}),
|
||||
)),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let rename = if &dialog_state == &DialogState::Rename {
|
||||
let action = move |state: &mut AppState| {
|
||||
state.dialog = None;
|
||||
if !state.dialog_text_field.is_empty() {
|
||||
state.rename().unwrap_or_else(|err| {
|
||||
state.error(format!(
|
||||
"Error renaming {:?} to {:?}:\n{}",
|
||||
state.selected_entry().unwrap().name,
|
||||
state.dialog_text_field,
|
||||
err
|
||||
));
|
||||
});
|
||||
}
|
||||
};
|
||||
Some((
|
||||
custom_label!("New name/location:"),
|
||||
text_input(
|
||||
state.dialog_text_field.clone(),
|
||||
|state: &mut AppState, input: String| state.dialog_text_field = input,
|
||||
)
|
||||
.on_enter(move |state, _| action(state)),
|
||||
flex_row((
|
||||
custom_button!("Rename", action, state.dialog_text_field.is_empty()),
|
||||
custom_button!("Cancel", |state: &mut AppState| {
|
||||
state.dialog = None;
|
||||
}),
|
||||
)),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let delete = if &dialog_state == &DialogState::Delete {
|
||||
Some((
|
||||
custom_label!("Are you sure you want to delete?"),
|
||||
flex_row((
|
||||
custom_button!("OK", |state: &mut AppState| {
|
||||
state.dialog = None;
|
||||
state.delete().unwrap_or_else(|err| {
|
||||
state.error(format!(
|
||||
"Error deleting {:?}:\n{}",
|
||||
state.selected_entry().unwrap().name,
|
||||
err
|
||||
));
|
||||
});
|
||||
}),
|
||||
custom_button!("Cancel", |state: &mut AppState| {
|
||||
state.dialog = None;
|
||||
}),
|
||||
)),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
sized_box(
|
||||
flex_col((error, new_file, new_dir, rename, delete))
|
||||
.corner_radius(6.)
|
||||
.background_color(BG)
|
||||
.border(BG_ALT, 2.)
|
||||
.padding(Padding::all(16.)),
|
||||
)
|
||||
.width(320.px())
|
||||
}
|
||||
|
||||
/// The primary app logic.
|
||||
pub fn app_main(state: &mut AppState) -> impl WidgetView<AppState> + use<> {
|
||||
zstack((
|
||||
flex_col((
|
||||
path_display(state),
|
||||
split(
|
||||
flex_col((
|
||||
dir_view(state).flex(1.0),
|
||||
entry_metadata(state),
|
||||
entry_ops(state),
|
||||
))
|
||||
.cross_axis_alignment(CrossAxisAlignment::Start)
|
||||
.padding(Padding::all(8.)),
|
||||
flex_col(if state.file_viewer.is_some() {
|
||||
Some((file_view().flex(1.0), file_ops(state)))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.cross_axis_alignment(CrossAxisAlignment::Fill),
|
||||
)
|
||||
.min_size(500.px(), 0.px())
|
||||
.bar_size(if state.file_viewer.is_some() {
|
||||
3.px()
|
||||
} else {
|
||||
0.px()
|
||||
})
|
||||
// To hide the file viewer when no file is open, we make the
|
||||
// splitter invisible and non-interactable
|
||||
.split_point(if state.file_viewer.is_some() {
|
||||
0.5
|
||||
} else {
|
||||
1.0
|
||||
})
|
||||
.draggable(state.file_viewer.is_some())
|
||||
.flex(1.0),
|
||||
))
|
||||
.cross_axis_alignment(CrossAxisAlignment::Fill)
|
||||
.background_color(BG),
|
||||
if let Some(dialog_state) = &state.dialog {
|
||||
Some(dialog(state, dialog_state.clone()))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
))
|
||||
}
|
||||
133
project3/src/files.rs
Normal file
133
project3/src/files.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
//! Datatypes and operations for manipulating the filesystem.
|
||||
use std::ffi::OsString;
|
||||
use std::fs::*;
|
||||
use std::io::*;
|
||||
use std::path::{Path, MAIN_SEPARATOR};
|
||||
|
||||
/// A file/subdirectory entry in a directory.
|
||||
#[derive(Clone)]
|
||||
pub struct EntryData {
|
||||
/// The index of the entry in the list.
|
||||
pub index: usize,
|
||||
/// The name of the entry.
|
||||
pub name: OsString,
|
||||
/// The metadata associated with the entry
|
||||
/// (size, permissions, etc).
|
||||
pub metadata: Metadata,
|
||||
}
|
||||
|
||||
/// Access and return the entries of the directory `dir`.
|
||||
///
|
||||
/// The entries are returned in code-point lexicographic order
|
||||
/// (usually the same as alphabetical order).
|
||||
pub fn get_entries(dir: &Path) -> Result<Vec<EntryData>> {
|
||||
let mut entries: Vec<_> = dir.read_dir()?.try_collect()?;
|
||||
entries.sort_unstable_by_key(|entry| entry.file_name());
|
||||
entries
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, entry)| {
|
||||
Ok::<_, Error>(EntryData {
|
||||
index: i,
|
||||
name: entry.file_name(),
|
||||
metadata: entry.metadata()?,
|
||||
})
|
||||
})
|
||||
.try_collect()
|
||||
}
|
||||
|
||||
/// Rename/Move the directory entry to the given location.
|
||||
///
|
||||
/// The string `to` is either the new name of the entry or the location
|
||||
/// of the directory to move the entry into. The latter is assumed if:
|
||||
/// 1. The path points to an existing non-empty directory, or
|
||||
/// 2. The path ends with a separator ("/" on Unix, "\" on Windows).
|
||||
///
|
||||
/// The target location's parent directories are automatically created.
|
||||
pub fn rename_entry(entry: &EntryData, to: &str) -> Result<()> {
|
||||
let to_path = Path::new(to);
|
||||
if to.ends_with(MAIN_SEPARATOR) || (to_path.is_dir() && to_path.read_dir()?.next().is_some()) {
|
||||
std::fs::create_dir_all(to_path)?;
|
||||
std::fs::rename(&entry.name, Path::join(to_path, &entry.name))?;
|
||||
} else {
|
||||
if let Some(parent) = to_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::rename(&entry.name, to_path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete the file/directory corresponding to `entry`.
|
||||
///
|
||||
/// Directories are deleted recursively.
|
||||
pub fn delete_entry(entry: &EntryData) -> Result<()> {
|
||||
if entry.metadata.is_dir() {
|
||||
std::fs::remove_dir_all(&entry.name)
|
||||
} else {
|
||||
std::fs::remove_file(&entry.name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Data associated with a currently open file.
|
||||
pub struct FileData {
|
||||
/// The name of the file.
|
||||
pub name: OsString,
|
||||
/// The file handle.
|
||||
pub handle: File,
|
||||
/// A buffer storing the contents of the file.
|
||||
pub buffer: String,
|
||||
/// Whether the file handle is read-only.
|
||||
pub readonly: bool,
|
||||
/// Whether the buffer is modified
|
||||
/// (different from the contents of the file).
|
||||
pub modified: bool,
|
||||
}
|
||||
|
||||
/// Open and return the file pointed to by `path`.
|
||||
pub fn open(path: impl AsRef<Path>) -> Result<FileData> {
|
||||
let mut readonly = false;
|
||||
// Try opening with write
|
||||
let mut handle = File::options()
|
||||
.write(true)
|
||||
.read(true)
|
||||
.open(&path)
|
||||
.or_else(|err| match err.kind() {
|
||||
ErrorKind::PermissionDenied => {
|
||||
// If `PermissionDenied` error, try opening read-only
|
||||
readonly = true;
|
||||
File::options().read(true).open(&path)
|
||||
}
|
||||
// Pass through all other errors
|
||||
_ => Err(err),
|
||||
})?;
|
||||
|
||||
// Allocate String buffer to correct size
|
||||
let size = handle.metadata()?.len() as usize;
|
||||
let mut buffer = String::with_capacity(size);
|
||||
handle.read_to_string(&mut buffer)?;
|
||||
|
||||
Ok(FileData {
|
||||
name: path.as_ref().file_name().unwrap().to_owned(),
|
||||
handle,
|
||||
buffer,
|
||||
readonly,
|
||||
modified: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Save the given file to disk.
|
||||
pub fn save(file: &mut FileData) -> Result<()> {
|
||||
if file.modified {
|
||||
// Move file cursor to beginning
|
||||
file.handle.rewind()?;
|
||||
// Truncate file
|
||||
file.handle.set_len(0)?;
|
||||
// Write contents of buffer
|
||||
file.handle.write_all(file.buffer.as_bytes())?;
|
||||
// Flush write buffer
|
||||
file.handle.flush()?;
|
||||
file.modified = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
28
project3/src/main.rs
Normal file
28
project3/src/main.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#![feature(iterator_try_collect)]
|
||||
use std::path::{PathBuf, MAIN_SEPARATOR_STR};
|
||||
|
||||
use winit::error::EventLoopError;
|
||||
use xilem::{EventLoop, WindowOptions, Xilem};
|
||||
|
||||
mod app;
|
||||
mod files;
|
||||
use app::*;
|
||||
|
||||
fn main() -> Result<(), EventLoopError> {
|
||||
// Get start directory, first available of:
|
||||
// - Current directory
|
||||
// - Home directory
|
||||
// - Filesystem root
|
||||
let start_dir = std::env::current_dir()
|
||||
.ok()
|
||||
.or_else(|| std::env::home_dir())
|
||||
.unwrap_or_else(|| PathBuf::from(MAIN_SEPARATOR_STR));
|
||||
|
||||
// Start GUI app
|
||||
let app = Xilem::new_simple(
|
||||
AppState::new(start_dir),
|
||||
app_main,
|
||||
WindowOptions::new("File Manager"),
|
||||
);
|
||||
app.run_in(EventLoop::with_user_event())
|
||||
}
|
||||
1
project3/test.nix
Normal file
1
project3/test.nix
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
44
shell.nix
44
shell.nix
|
|
@ -1,13 +1,37 @@
|
|||
{ mkShell,
|
||||
llvmPackages_21,
|
||||
dotnet-sdk
|
||||
{ pkgs ? import <nixpkgs> {},
|
||||
crane ? builtins.getFlake "github:ipetkov/crane",
|
||||
fenix ? builtins.getFlake "github:nix-community/fenix",
|
||||
}:
|
||||
|
||||
mkShell {
|
||||
packages = [
|
||||
# GCC is provided by default
|
||||
|
||||
# Provides clangd
|
||||
llvmPackages_21.clang-tools
|
||||
let
|
||||
fenixPkgs = fenix.packages.${pkgs.system};
|
||||
toolchain = fenixPkgs.complete.withComponents [
|
||||
"rustc" "rust-std" "cargo" "rust-docs" "rustfmt" "clippy" "rust-src"
|
||||
];
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain toolchain;
|
||||
|
||||
# Rust dev tools (for P3)
|
||||
rustPackages = with pkgs; [
|
||||
pkg-config
|
||||
fontconfig
|
||||
wayland
|
||||
wayland-protocols
|
||||
wayland-scanner
|
||||
vulkan-loader
|
||||
libxkbcommon
|
||||
xorg.libxcb
|
||||
xorg.libX11
|
||||
xorg.libXcursor
|
||||
xorg.libXi
|
||||
xorg.libXrandr
|
||||
xorg.libXxf86vm
|
||||
];
|
||||
in craneLib.devShell {
|
||||
packages = rustPackages ++ [
|
||||
fenixPkgs.rust-analyzer
|
||||
|
||||
# C dev tools
|
||||
pkgs.gcc
|
||||
pkgs.llvmPackages_21.clang-tools
|
||||
];
|
||||
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath rustPackages}";
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue