Skip to content

Commit 4d97c31

Browse files
committed
Add Zellij support
1 parent 95b6160 commit 4d97c31

5 files changed

Lines changed: 146 additions & 8 deletions

File tree

src/app_state.rs

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use anyhow::{Context, Error, Result, bail};
22
use crossterm::{QueueableCommand, cursor, terminal};
3+
use serde::Deserialize;
34
use std::{
45
collections::HashSet,
56
env,
@@ -11,7 +12,7 @@ use std::{
1112
atomic::{AtomicUsize, Ordering::Relaxed},
1213
mpsc,
1314
},
14-
thread,
15+
thread::{self, JoinHandle},
1516
};
1617

1718
use crate::{
@@ -49,6 +50,44 @@ pub enum CheckProgress {
4950
Pending,
5051
}
5152

53+
#[derive(Deserialize)]
54+
struct Pane {
55+
id: u32,
56+
}
57+
58+
#[must_use]
59+
pub struct EditCmdJoinHandle(Option<JoinHandle<Result<(String, u32)>>>);
60+
61+
fn parse_pane_id(b: &[u8]) -> Option<(String, u32)> {
62+
// Remove newline
63+
let b = b.get("terminal_".len()..b.len().saturating_sub(1))?;
64+
let id_str = str::from_utf8(b).ok()?;
65+
66+
let (first, rest) = b.split_first()?;
67+
let mut id = u32::from(first - b'0');
68+
69+
for c in rest {
70+
id = 10 * id + u32::from(c - b'0');
71+
}
72+
73+
Some((id_str.to_owned(), id))
74+
}
75+
76+
fn close_pane(pane_id: &str) -> Result<()> {
77+
Command::new("zellij")
78+
.arg("action")
79+
.arg("close-pane")
80+
.arg("-p")
81+
.arg(pane_id)
82+
.stdin(Stdio::null())
83+
.stdout(Stdio::null())
84+
.stderr(Stdio::null())
85+
.status()
86+
.context("Failed to run `zellij action close-pane -p ID`")?;
87+
88+
Ok(())
89+
}
90+
5291
pub struct AppState {
5392
current_exercise_ind: usize,
5493
exercises: Vec<Exercise>,
@@ -61,12 +100,15 @@ pub struct AppState {
61100
official_exercises: bool,
62101
cmd_runner: CmdRunner,
63102
emit_file_links: bool,
103+
zellij: bool,
104+
open_pane: Option<(String, u32, usize)>,
64105
}
65106

66107
impl AppState {
67108
pub fn new(
68109
exercise_infos: Vec<ExerciseInfo>,
69110
final_message: &'static str,
111+
zellij: bool,
70112
) -> Result<(Self, StateFileStatus)> {
71113
let cmd_runner = CmdRunner::build()?;
72114
let mut state_file = OpenOptions::new()
@@ -175,6 +217,8 @@ impl AppState {
175217
cmd_runner,
176218
// VS Code has its own file link handling
177219
emit_file_links: env::var_os("TERM_PROGRAM").is_none_or(|v| v != "vscode"),
220+
zellij,
221+
open_pane: None,
178222
};
179223

180224
Ok((slf, state_file_status))
@@ -553,6 +597,86 @@ impl AppState {
553597

554598
Ok(())
555599
}
600+
601+
pub fn close_pane(&mut self) -> Result<()> {
602+
if let Some((pane_id_str, _, _)) = self.open_pane.take() {
603+
close_pane(&pane_id_str)?;
604+
}
605+
606+
Ok(())
607+
}
608+
609+
pub fn edit_cmd(&mut self) -> Result<EditCmdJoinHandle> {
610+
if !self.zellij {
611+
return Ok(EditCmdJoinHandle(None));
612+
}
613+
614+
let open_pane = self.open_pane.take();
615+
let current_exercise_ind = self.current_exercise_ind;
616+
let mut edit_cmd = Command::new("zellij");
617+
edit_cmd
618+
.arg("action")
619+
.arg("edit")
620+
.arg(&self.current_exercise().path)
621+
.stdin(Stdio::null())
622+
.stderr(Stdio::null());
623+
624+
let handle = thread::Builder::new()
625+
.spawn(move || {
626+
if let Some((pane_id_str, pane_id, exercise_ind)) = open_pane {
627+
if exercise_ind == current_exercise_ind {
628+
// Check if the pane is still open
629+
let mut output = Command::new("zellij")
630+
.arg("action")
631+
.arg("list-panes")
632+
.arg("-j")
633+
.stdin(Stdio::null())
634+
.stderr(Stdio::null())
635+
.output()
636+
.context("Failed to run `zellij action list-panes -j`")?;
637+
638+
if !output.status.success() {
639+
bail!("`zellij action list-panes -j` didn't exit successfully");
640+
}
641+
642+
// Remove newline
643+
output.stdout.pop();
644+
645+
let panes = serde_json::de::from_slice::<Vec<Pane>>(&output.stdout)
646+
.context(
647+
"Failed to parse the output of `zellij action list-panes -j`",
648+
)?;
649+
650+
if panes.iter().any(|pane| pane.id == pane_id) {
651+
return Ok((pane_id_str, pane_id));
652+
}
653+
} else {
654+
close_pane(&pane_id_str)?;
655+
}
656+
}
657+
658+
let output = edit_cmd.output()?;
659+
660+
if !output.status.success() {
661+
bail!("Failed to open a new Zellij editor pane");
662+
}
663+
664+
parse_pane_id(&output.stdout)
665+
.context("Failed to parse the ID of the new Zellij pane")
666+
})
667+
.context("Failed to spawn a thread to open and close Zellij panes")?;
668+
669+
Ok(EditCmdJoinHandle(Some(handle)))
670+
}
671+
672+
pub fn join_edit_cmd(&mut self, handle: EditCmdJoinHandle) -> Result<()> {
673+
if let Some(handle) = handle.0 {
674+
let (pane_id_str, pane_id) = handle.join().unwrap()?;
675+
self.open_pane = Some((pane_id_str, pane_id, self.current_exercise_ind));
676+
}
677+
678+
Ok(())
679+
}
556680
}
557681

558682
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
@@ -608,6 +732,8 @@ mod tests {
608732
official_exercises: true,
609733
cmd_runner: CmdRunner::build().unwrap(),
610734
emit_file_links: true,
735+
zellij: false,
736+
open_pane: None,
611737
};
612738

613739
let mut assert = |done: [bool; 3], expected: [Option<usize>; 3]| {

src/cli.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,33 @@ pub struct Args {
99
#[command(subcommand)]
1010
pub command: Option<Command>,
1111
/// Manually run the current exercise using `r` in the watch mode.
12-
/// Only use this if Rustlings fails to detect exercise file changes.
12+
/// Only use this if Rustlings fails to detect exercise file changes
1313
#[arg(long)]
1414
pub manual_run: bool,
15+
/// Open the current exercise in a new Zellij pane and close the last one if exists
16+
#[arg(long)]
17+
pub zellij: bool,
1518
}
1619

1720
#[derive(Subcommand)]
1821
pub enum Command {
1922
/// Initialize the official Rustlings exercises
2023
Init,
21-
/// Run a single exercise. Runs the next pending exercise if the exercise name is not specified
24+
/// Run a single exercise.
25+
/// Runs the next pending exercise if the exercise name is not specified
2226
Run {
2327
/// The name of the exercise
2428
name: Option<String>,
2529
},
26-
/// Check all the exercises, marking them as done or pending accordingly.
30+
/// Check all the exercises, marking them as done or pending accordingly
2731
CheckAll,
2832
/// Reset a single exercise
2933
Reset {
3034
/// The name of the exercise
3135
name: String,
3236
},
33-
/// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified
37+
/// Show a hint.
38+
/// Shows the hint of the next pending exercise if the exercise name is not specified
3439
Hint {
3540
/// The name of the exercise
3641
name: Option<String>,

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ fn main() -> Result<ExitCode> {
6161
let (mut app_state, state_file_status) = AppState::new(
6262
info_file.exercises,
6363
info_file.final_message.unwrap_or_default(),
64+
args.zellij,
6465
)?;
6566

6667
// Show the welcome message if the state file doesn't exist yet.

src/watch/state.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,16 @@ impl<'a> WatchState<'a> {
7878
// Ignore any input until running the exercise is done.
7979
let _input_pause_guard = InputPauseGuard::scoped_pause();
8080

81-
self.show_hint = false;
82-
8381
writeln!(
8482
stdout,
8583
"\nChecking the exercise `{}`. Please wait…",
8684
self.app_state.current_exercise().name,
8785
)?;
8886

87+
let edit_cmd_handle = self.app_state.edit_cmd()?;
88+
89+
self.show_hint = false;
90+
8991
let success = self
9092
.app_state
9193
.current_exercise()
@@ -105,7 +107,9 @@ impl<'a> WatchState<'a> {
105107
self.done_status = DoneStatus::Pending;
106108
}
107109

110+
self.app_state.join_edit_cmd(edit_cmd_handle)?;
108111
self.render(stdout)?;
112+
109113
Ok(())
110114
}
111115

@@ -127,9 +131,10 @@ impl<'a> WatchState<'a> {
127131

128132
match answer[0] {
129133
b'y' | b'Y' => {
134+
self.app_state.close_pane()?;
130135
self.app_state.reset_current_exercise()?;
131136

132-
// The file watcher reruns the exercise otherwise.
137+
// The file watcher reruns the exercise otherwise
133138
if self.manual_run {
134139
self.run_current_exercise(stdout)?;
135140
}

tmp.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
226.867688ms

0 commit comments

Comments
 (0)