Skip to content

Commit 23100c1

Browse files
authored
Merge pull request #9 from BakerNet/more-tools
ls -l support
2 parents 8499ea4 + ea336b4 commit 23100c1

2 files changed

Lines changed: 178 additions & 25 deletions

File tree

src/app/terminal/fs.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,4 +308,49 @@ impl Target {
308308
pub fn is_executable(&self) -> bool {
309309
matches!(self, Self::File(File::MinesSh | File::Nav(_)))
310310
}
311+
312+
pub fn full_permissions(&self) -> &'static str {
313+
match self {
314+
Self::Dir(_) => "drwxr-xr-x",
315+
Self::File(_) if self.is_executable() => "-rwxr-xr-x",
316+
Self::File(_) => "-rw-r--r--",
317+
Self::Invalid => "?---------",
318+
}
319+
}
320+
321+
pub fn link_count(&self, blog_post_count: usize) -> u32 {
322+
match self {
323+
Self::Dir(Dir::Base) => 6 + 2, // mines.sh, thanks.txt, .zshrc, nav.rs, blog/, cv/ + . + ..
324+
Self::Dir(Dir::Blog) => (blog_post_count + 1 + 2) as u32, // posts + nav.rs + . + ..
325+
Self::Dir(Dir::CV) => 1 + 2, // nav.rs + . + ..
326+
Self::Dir(Dir::BlogPost(_)) => 1 + 2, // nav.rs + . + ..
327+
Self::File(_) => 1, // Regular files have 1 link
328+
Self::Invalid => 0,
329+
}
330+
}
331+
332+
pub fn owner(&self) -> &'static str {
333+
"hansbaker"
334+
}
335+
336+
pub fn group(&self) -> &'static str {
337+
match self {
338+
Self::Dir(_) => "staff",
339+
Self::File(File::MinesSh) => "wheel", // Executable gets wheel group
340+
Self::File(File::Nav(_)) => "wheel", // nav.rs is executable
341+
Self::File(_) => "staff",
342+
Self::Invalid => "staff",
343+
}
344+
}
345+
346+
pub fn size(&self) -> u64 {
347+
match self {
348+
Self::Dir(_) => 128, // Directories show standard size
349+
Self::File(File::MinesSh) => 156, // Size of the mines.sh script
350+
Self::File(File::ThanksTxt) => 77, // Size of thanks.txt
351+
Self::File(File::ZshRc) => 1024, // .zshrc is larger
352+
Self::File(File::Nav(_)) => 512, // nav.rs files
353+
Self::Invalid => 0,
354+
}
355+
}
311356
}

src/app/terminal/fs_tools.rs

Lines changed: 133 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::sync::Arc;
22

3-
use leptos::{either::*, prelude::*};
3+
use leptos::prelude::*;
44
use leptos_router::components::*;
55

66
use crate::app::terminal::fs::DirContentItem;
@@ -27,18 +27,23 @@ impl Executable for LsCommand {
2727
is_output_tty: bool,
2828
) -> CommandRes {
2929
let mut all = false;
30+
let mut long_format = false;
3031
let (options, mut target_paths) = parse_multitarget(args);
31-
let invalid = options.iter().find(|c| **c != 'a');
32+
let invalid = options.iter().find(|c| **c != 'a' && **c != 'l');
3233
if let Some(c) = invalid {
3334
let c = c.to_owned();
3435
let error_msg = format!(
3536
r#"ls: invalid option -- '{c}'
36-
This version of ls only supports option 'a'"#
37+
This version of ls only supports options 'a' and 'l'"#
3738
);
3839
return CommandRes::new().with_error().with_stderr(error_msg);
3940
}
40-
if !options.is_empty() {
41-
all = true;
41+
for option in &options {
42+
match option {
43+
'a' => all = true,
44+
'l' => long_format = true,
45+
_ => unreachable!("Invalid options should be caught above"),
46+
}
4247
}
4348
if target_paths.is_empty() {
4449
target_paths = vec![""];
@@ -84,6 +89,7 @@ This version of ls only supports option 'a'"#
8489
let is_multi =
8590
dir_targets.len() > 1 || !dir_targets.is_empty() && !file_targets.is_empty();
8691
let all_captured = all;
92+
let long_format_captured = long_format;
8793
let path_owned = path.to_owned();
8894
result = result.with_stdout_view(Arc::new(move || {
8995
let mut all_views = Vec::new();
@@ -95,6 +101,8 @@ This version of ls only supports option 'a'"#
95101
.map(|(s, t)| DirContentItem(s.to_string(), t.to_owned()))
96102
.collect(),
97103
base: path_owned.clone(),
104+
long_format: long_format_captured,
105+
blog_post_count: posts.len(),
98106
})
99107
.into_any(),
100108
);
@@ -119,6 +127,8 @@ This version of ls only supports option 'a'"#
119127
LsView(LsViewProps {
120128
items: d.contents(&posts, all_captured),
121129
base: d.base(),
130+
long_format: long_format_captured,
131+
blog_post_count: posts.len(),
122132
})
123133
.into_any(),
124134
);
@@ -137,7 +147,19 @@ This version of ls only supports option 'a'"#
137147
if !stdout_text.is_empty() {
138148
stdout_text.push('\n');
139149
}
140-
stdout_text.push_str(f.name());
150+
if long_format {
151+
stdout_text.push_str(&format!(
152+
"{} {:2} {:8} {:8} {:>6} {}",
153+
target.full_permissions(),
154+
target.link_count(self.blog_posts.len()),
155+
target.owner(),
156+
target.group(),
157+
target.size(),
158+
f.name()
159+
));
160+
} else {
161+
stdout_text.push_str(f.name());
162+
}
141163
}
142164
}
143165
}
@@ -156,7 +178,19 @@ This version of ls only supports option 'a'"#
156178
if i > 0 || (!is_multi && !stdout_text.is_empty()) {
157179
stdout_text.push('\n');
158180
}
159-
stdout_text.push_str(item.text_content());
181+
if long_format {
182+
stdout_text.push_str(&format!(
183+
"{} {:2} {:8} {:8} {:>6} {}",
184+
item.1.full_permissions(),
185+
item.1.link_count(self.blog_posts.len()),
186+
item.1.owner(),
187+
item.1.group(),
188+
item.1.size(),
189+
item.text_content()
190+
));
191+
} else {
192+
stdout_text.push_str(item.text_content());
193+
}
160194
}
161195
}
162196

@@ -170,36 +204,110 @@ This version of ls only supports option 'a'"#
170204
}
171205

172206
#[component]
173-
fn LsView(items: Vec<DirContentItem>, base: String) -> impl IntoView {
207+
fn LsView(
208+
items: Vec<DirContentItem>,
209+
base: String,
210+
#[prop(default = false)] long_format: bool,
211+
#[prop(default = 0)] blog_post_count: usize,
212+
) -> impl IntoView {
174213
let dir_class = "text-blue";
175214
let ex_class = "text-green";
176-
let render_func = {
177-
move |s: DirContentItem| {
178-
if matches!(s.1, Target::Dir(_)) {
215+
216+
// Create modified items for long format display if needed
217+
let display_items = if long_format {
218+
items
219+
.into_iter()
220+
.map(|item| {
221+
let long_info = format!(
222+
"{} {:2} {:8} {:8} {:>6} {}",
223+
item.1.full_permissions(),
224+
item.1.link_count(blog_post_count),
225+
item.1.owner(),
226+
item.1.group(),
227+
item.1.size(),
228+
item.0
229+
);
230+
DirContentItem(long_info, item.1)
231+
})
232+
.collect::<Vec<_>>()
233+
} else {
234+
items
235+
};
236+
237+
if long_format {
238+
let long_render_func = move |s: DirContentItem| {
239+
// Find the last space before the filename to preserve original formatting
240+
let last_space_pos = s.0.rfind(' ').unwrap();
241+
let metadata_with_spaces = s.0[..last_space_pos].to_string();
242+
let filename = s.0[last_space_pos + 1..].to_string();
243+
244+
// Create the styled filename part
245+
let styled_filename = if matches!(s.1, Target::Dir(_)) {
179246
let base = if base == "/" { "" } else { &base };
180-
let href = if s.0 == "." {
247+
let href = if filename == "." {
181248
base.to_string()
182249
} else {
183-
format!("{}/{}", base, s.0)
250+
format!("{}/{}", base, filename)
184251
};
185-
// note - adding extra space because trimming trailing '/'
186-
EitherOf3::A(view! {
252+
view! {
187253
<A href=href attr:class=dir_class>
188-
{s.text_content().to_string()}
254+
{filename}
189255
</A>
190-
})
256+
}.into_any()
191257
} else if s.1.is_executable() {
192-
EitherOf3::B(view! { <span class=ex_class>{s.text_content()}</span> })
258+
view! { <span class=ex_class>{filename}</span> }.into_any()
193259
} else {
194-
EitherOf3::C(view! { <span>{s.text_content()}</span> })
195-
}
260+
view! { <span>{filename}</span> }.into_any()
261+
};
262+
263+
view! { <span>{metadata_with_spaces} " " {styled_filename}</span> }
196264
.into_any()
265+
};
266+
267+
view! {
268+
<div>
269+
{display_items
270+
.into_iter()
271+
.map(|item| {
272+
view! {
273+
{long_render_func(item)}
274+
"\n"
275+
}
276+
})
277+
.collect_view()}
278+
</div>
197279
}
198-
};
199-
view! {
200-
<div>
201-
<ColumnarView items render_func />
202-
</div>
280+
.into_any()
281+
} else {
282+
let short_render_func = {
283+
move |s: DirContentItem| {
284+
if matches!(s.1, Target::Dir(_)) {
285+
let base = if base == "/" { "" } else { &base };
286+
let href = if s.0 == "." {
287+
base.to_string()
288+
} else {
289+
format!("{}/{}", base, s.0)
290+
};
291+
view! {
292+
<A href=href attr:class=dir_class>
293+
{s.text_content().to_string()}
294+
</A>
295+
}
296+
.into_any()
297+
} else if s.1.is_executable() {
298+
view! { <span class=ex_class>{s.text_content()}</span> }.into_any()
299+
} else {
300+
view! { <span>{s.text_content()}</span> }.into_any()
301+
}
302+
}
303+
};
304+
305+
view! {
306+
<div>
307+
<ColumnarView items=display_items render_func=short_render_func />
308+
</div>
309+
}
310+
.into_any()
203311
}
204312
}
205313

0 commit comments

Comments
 (0)