2 Commits

Author SHA1 Message Date
6bdb38bcee Something stupid
Some checks failed
ci/woodpecker/push/pre_commit_test Pipeline was successful
ci/woodpecker/push/code_tests Pipeline failed
2025-12-18 17:33:00 +01:00
Nikolai Rodionov
49e4f05b5e Try starting monolith
Some checks failed
ci/woodpecker/push/code_tests Pipeline failed
ci/woodpecker/push/pre_commit_test Pipeline was successful
2025-12-18 16:03:54 +01:00
35 changed files with 694 additions and 2073 deletions

View File

@@ -1,2 +0,0 @@
[codespell]
ignore-words-list = ratatui

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
resources/** filter=lfs diff=lfs merge=lfs -text

View File

@@ -1,10 +1,9 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
rev: v2.3.0
hooks:
- id: check-added-large-files
- id: check-merge-conflict
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/codespell-project/codespell

1932
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,20 @@
[workspace]
resolver = "3"
members = ["engine", "examples/jack-playback", "examples/jack-sine", "lib", "tui"]
[package]
name = "termix"
version = "0.1.0"
edition = "2024"
[workspace.dependencies]
[dependencies]
clap = { version = "4.5.53", features = ["derive"] }
jack = { version = "0.13.3", optional = true}
pulseaudio = { version = "0.3.1", optional = true}
alsa = { version = "0.10.0", optional = true}
symphonia = { version = "0.5.5", features = ["mp3"] }
coreaudio-rs = { version = "0.13.0", optional = true }
cpal = { version = "0.16.0", optional = true }
[features]
jack = ["dep:jack"]
pulseaudio = ["dep:pulseaudio"]
alsa = ["dep:alsa"]
coreaudio = ["dep:coreaudio-rs"]
cpal = ["dep:cpal"]

View File

@@ -1,8 +1,2 @@
# termix
[![status-badge](https://ci.badhouseplants.net/api/badges/19/status.svg)](https://ci.badhouseplants.net/repos/19)
# Requirenments
On all systems:
- protoc

View File

@@ -1,22 +0,0 @@
[package]
name = "engine"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4.5.53", features = ["derive"] }
coreaudio-rs = { version = "0.13.0", optional = true }
crossbeam-channel = "0.5.15"
env_logger = "0.11.8"
jack = {version = "0.13.3", optional = true }
lib = { path = "../lib/" }
log = "0.4.28"
prost = "0.14.1"
tokio = { version = "1.48.0", features = ["rt-multi-thread"] }
tonic = "0.14.2"
tonic-prost = "0.14.2"
tonic-reflection = "0.14.2"
[features]
jack = ["dep:jack"]
coreaudio = ["dep:coreaudio-rs"]

View File

@@ -1,26 +0,0 @@
use crate::audio_engine::AudioBackend;
pub(crate) struct CoreAudioBackend {}
impl CoreAudioBackend {}
#[cfg(feature = "coreaudio")]
impl AudioBackend for CoreAudioBackend {
fn start_client(&mut self) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
fn describe_backend() -> Result<super::BackendDescription, Box<dyn std::error::Error>> {
todo!()
}
}
#[cfg(not(feature = "coreaudio"))]
impl AudioBackend for CoreAudioBackend {
fn start_client(&mut self) -> Result<(), Box<dyn std::error::Error>> {
todo!()
}
fn describe_backend() -> Result<super::BackendDescription, Box<dyn std::error::Error>> {
todo!()
}
}

View File

@@ -1,19 +0,0 @@
use crate::audio_engine::AudioBackend;
struct DummyAudioBackend {}
impl DummyAudioBackend {
fn new() -> Self {
Self {}
}
}
impl AudioBackend for DummyAudioBackend {
fn start_client(&mut self) -> Result<(), Box<dyn std::error::Error>> {
todo!()
}
fn describe_backend() -> Result<super::BackendDescription, Box<dyn std::error::Error>> {
todo!()
}
}

View File

@@ -1,75 +0,0 @@
use crate::audio_engine::AudioBackend;
use crossbeam_channel::bounded;
use log::{info, warn};
use std::error::Error;
#[cfg(feature = "jack")]
use jack;
#[cfg(feature = "jack")]
use jack::ClientOptions;
pub(crate) struct JackAudioBackend {
pub(crate) feature_jack: bool,
pub(crate) running: bool,
status: JackStatus,
}
impl JackAudioBackend {
pub(crate) fn new() -> Self {
let feature_jack = cfg!(feature = "jack");
// TODO: It should be retrieved from the system
let running = true;
let status = JackStatus::default();
Self {
feature_jack,
running,
status,
}
}
}
#[cfg(feature = "jack")]
#[derive(Default)]
pub(crate) struct JackStatus {
client: Option<jack::Client>,
status: Option<jack::ClientStatus>,
}
#[cfg(feature = "jack")]
impl AudioBackend for JackAudioBackend {
fn start_client(&mut self) -> Result<(), Box<dyn Error>> {
Ok(())
}
// Get the possible input and output ports to use them for the session initialization
fn describe_backend() -> Result<super::BackendDescription, Box<dyn Error>> {
use jack::{Client, PortFlags};
use crate::audio_engine::BackendDescription;
let (client, _) = Client::new("list_ports", ClientOptions::empty())?;
let ports_in = client.ports(None, None, PortFlags::IS_INPUT);
let ports_out = client.ports(None, None, PortFlags::IS_OUTPUT);
let output = BackendDescription {
audio_devices_out: ports_out,
audio_devices_in: ports_in,
};
Ok(output)
}
}
#[cfg(not(feature = "jack"))]
#[derive(Default)]
pub(crate) struct JackStatus {}
#[cfg(not(feature = "jack"))]
impl AudioBackend for JackAudioBackend {
fn start_client(&mut self) -> Result<(), Box<dyn Error>> {
warn!("jack support is not enabled");
Ok(())
}
fn describe_backend() -> Result<super::BackendDescription, Box<dyn Error>> {
unimplemented!("jack is disabled")
}
}

View File

@@ -1,17 +0,0 @@
use std::error::Error;
pub(crate) mod coreaudio_ab;
pub(crate) mod dummy_ab;
pub(crate) mod jack_ab;
pub(crate) struct BackendDescription {
pub(crate) audio_devices_out: Vec<String>,
pub(crate) audio_devices_in: Vec<String>,
}
pub(crate) trait AudioBackend {
// Start a audio backend client
// It should be executed either on the startup,
// or when the audio backend is switched
fn start_client(&mut self) -> Result<(), Box<dyn Error>>;
fn describe_backend() -> Result<BackendDescription, Box<dyn Error>>;
}

View File

@@ -1,9 +0,0 @@
use crate::control_pane::ControlPane;
struct Dummy {}
impl ControlPane for Dummy {
async fn start_server(&self) -> Result<(), Box<dyn std::error::Error>> {
todo!()
}
}

View File

@@ -1,125 +0,0 @@
use std::result::Result;
use lib::termix::{
self,
audio_backend::{
AudioBackendDescription, Backend, BackendList, DesiredAudioBacked, FILE_DESCRIPTOR_SET,
SupportedAudioBackends,
audio_backend_rpc_server::{AudioBackendRpc, AudioBackendRpcServer},
},
};
use log::info;
use tonic::{Response, Status, transport::Server};
use tonic_reflection::server;
use crate::{
audio_engine::{
AudioBackend,
jack_ab::{self, JackAudioBackend},
},
control_pane::ControlPane,
};
pub(crate) struct Grpc {
pub(crate) port: i32,
pub(crate) enable_reflections: bool,
}
impl ControlPane for Grpc {
async fn start_server(&self) -> Result<(), Box<dyn std::error::Error>> {
info!("starting the grpc server on port {}", self.port);
// TODO: Use the port from self
let addr = "[::1]:50051".parse()?;
let mut server = Server::builder();
let audio_backend_reflections = server::Builder::configure()
.register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET)
.build_v1()
.unwrap();
let audio_backend = TermixAudioBackend::default();
server
.add_service(audio_backend_reflections)
.add_service(AudioBackendRpcServer::new(audio_backend))
.serve(addr)
.await?;
Ok(())
}
}
#[derive(Debug, Default)]
pub struct TermixAudioBackend {}
#[tonic::async_trait]
impl AudioBackendRpc for TermixAudioBackend {
async fn stop_client(
&self,
_: tonic::Request<()>,
) -> Result<tonic::Response<()>, tonic::Status> {
todo!()
}
async fn start_client(
&self,
request: tonic::Request<DesiredAudioBacked>,
) -> Result<tonic::Response<()>, tonic::Status> {
info!("starting the audio backend client");
match request.get_ref().backend() {
SupportedAudioBackends::AbUnspecified => {
unimplemented!("unsupported backend");
}
SupportedAudioBackends::AbJack => {
info!("trying to use JACK as the backend");
let mut ab = jack_ab::JackAudioBackend::new();
ab.start_client();
}
SupportedAudioBackends::AbCoreaudio => todo!(),
};
Ok(Response::new(()))
}
async fn describe_backend(
&self,
request: tonic::Request<DesiredAudioBacked>,
) -> Result<tonic::Response<AudioBackendDescription>, tonic::Status> {
info!("Describing the audio backend");
match request.get_ref().backend() {
SupportedAudioBackends::AbUnspecified => return Err(Status::not_found("backend os not specified")),
SupportedAudioBackends::AbCoreaudio => todo!(),
SupportedAudioBackends::AbJack => {
let backend_desc= match jack_ab::JackAudioBackend::describe_backend(){
Ok(desc) => desc,
Err(err) => return Err(Status::internal(err.to_string())),
};
Ok(Response::new(AudioBackendDescription{
core_audio_description: None,
input_devices: backend_desc.audio_devices_in,
output_devices: backend_desc.audio_devices_out
}))
}
}
}
async fn init_connection(
&self,
request: tonic::Request<()>,
) -> Result<tonic::Response<()>, tonic::Status> {
info!("initializing the connection to the audio backend");
todo!()
}
async fn get_available_backends(
&self,
_: tonic::Request<()>,
) -> Result<Response<BackendList>, tonic::Status> {
info!("discovering available backends");
let mut response = BackendList::default();
let jack = JackAudioBackend::new();
if jack.feature_jack {
response.backends.push(Backend {
name: "jack".to_string(),
});
}
Ok(Response::new(response))
}
}

View File

@@ -1,6 +0,0 @@
pub(crate) mod dummy;
pub(crate) mod grpc;
pub(crate) trait ControlPane {
async fn start_server(&self) -> Result<(), Box<dyn std::error::Error>>;
}

View File

@@ -1,29 +0,0 @@
pub(crate) mod audio_engine;
pub(crate) mod control_pane;
use crate::control_pane::ControlPane;
use clap::Parser;
/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
#[arg(long, default_value_t = 50051)]
grpc_port: i32,
#[arg(long, default_value_t = true)]
grpc_enable_reflections: bool,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
let args = Args::parse();
let grpc_control_pane = control_pane::grpc::Grpc {
port: args.grpc_port,
enable_reflections: args.grpc_enable_reflections,
};
grpc_control_pane.start_server().await?;
Ok(())
}

View File

@@ -1,7 +0,0 @@
[package]
name = "jack-playback"
version = "0.1.0"
edition = "2024"
[dependencies]
jack = "0.13.3"

View File

@@ -1,5 +0,0 @@
fn main() {
// 1. Create client
let (client, _status) =
jack::Client::new("rust_jack_simple", jack::ClientOptions::default()).unwrap();
}

View File

@@ -1,8 +0,0 @@
[package]
name = "jack-sine"
version = "0.1.0"
edition = "2024"
[dependencies]
crossbeam-channel = "0.5.15"
jack = "0.13.3"

View File

@@ -1 +0,0 @@
Set this on mac export DYLD_FALLBACK_LIBRARY_PATH="$(brew --prefix jack)/lib:${DYLD_FALLBACK_LIBRARY_PATH:-}"

View File

@@ -1,100 +0,0 @@
//! Sine wave generator with frequency configuration exposed through standard
//! input.
use crossbeam_channel::bounded;
use jack::{PortFlags, PortSpec};
use std::io;
use std::str::FromStr;
fn main() {
// 1. open a client
let (client, _status) =
jack::Client::new("rust_jack_sine", jack::ClientOptions::default()).unwrap();
let ports = client.ports(None, Some(jack::AudioIn::default().jack_port_type()), PortFlags::empty());
println!("All JACK ports:");
for port in ports {
println!("{}", port);
}
// 2. register port
let out_port = client
.register_port("sine_out", jack::AudioOut::default())
.unwrap();
// 3. define process callback handler
let (tx, rx) = bounded(1_000_000);
struct State {
out_port: jack::Port<jack::AudioOut>,
rx: crossbeam_channel::Receiver<f64>,
frequency: f64,
frame_t: f64,
time: f64,
}
let process = jack::contrib::ClosureProcessHandler::with_state(
State {
out_port,
rx,
frequency: 220.0,
frame_t: 1.0 / client.sample_rate() as f64,
time: 0.0,
},
|state, _, ps| -> jack::Control {
// Get output buffer
let out = state.out_port.as_mut_slice(ps);
// Check frequency requests
while let Ok(f) = state.rx.try_recv() {
state.time = 0.0;
state.frequency = f;
}
// Write output
for v in out.iter_mut() {
let x = state.frequency * state.time * 2.0 * std::f64::consts::PI;
let y = x.sin();
*v = y as f32;
state.time += state.frame_t;
}
// Continue as normal
jack::Control::Continue
},
move |_, _, _| jack::Control::Continue,
);
// 4. Activate the client. Also connect the ports to the system audio.
let active_client = client.activate_async((), process).unwrap();
active_client
.as_client()
.connect_ports_by_name("rust_jack_sine:sine_out", "system:playback_1")
.unwrap();
active_client
.as_client()
.connect_ports_by_name("rust_jack_sine:sine_out", "system:playback_2")
.unwrap();
// processing starts here
// 5. wait or do some processing while your handler is running in real time.
println!("Enter an integer value to change the frequency of the sine wave.");
while let Some(f) = read_freq() {
tx.send(f).unwrap();
}
// 6. Optional deactivate. Not required since active_client will deactivate on
// drop, though explicit deactivate may help you identify errors in
// deactivate.
if let Err(err) = active_client.deactivate() {
eprintln!("JACK exited with error: {err}");
};
}
/// Attempt to read a frequency from standard in. Will block until there is
/// user input. `None` is returned if there was an error reading from standard
/// in, or the retrieved string wasn't a compatible u16 integer.
fn read_freq() -> Option<f64> {
let mut user_input = String::new();
match io::stdin().read_line(&mut user_input) {
Ok(_) => u16::from_str(user_input.trim()).ok().map(|n| n as f64),
Err(_) => None,
}
}

View File

@@ -1,16 +0,0 @@
[package]
name = "lib"
version = "0.1.0"
edition = "2024"
[dependencies]
prost = "0.14.1"
tokio = { version = "1.48.0", features = ["rt-multi-thread"] }
tonic = "0.14.2"
tonic-prost = "0.14.2"
uuid = { version = "1.18.1", features = ["v4"] }
[build-dependencies]
prost-build = "0.14.1"
prost-types = "0.14.1"
tonic-prost-build = "0.14.2"

View File

@@ -1,29 +0,0 @@
use std::{env, fs, io::Result, path::PathBuf};
fn main() -> Result<()> {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
//let proto_dir = "proto";
//let paths = fs::read_dir(proto_dir).unwrap();
//for path in paths {
// let path_str = path.unwrap().path().to_str().unwrap().to_string();
// let descriptor = format!("{}_descriptor.bin", path_str);
// tonic_prost_build::configure()
// .file_descriptor_set_path(out_dir.join("audio_backend_descriptor.bin"))
// .compile_protos(&["proto/audio_backend.proto"], &["proto"])
// .unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
//}
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
tonic_prost_build::configure()
.file_descriptor_set_path(out_dir.join("audio_backend_descriptor.bin"))
.compile_protos(&["proto/audio_backend.proto"], &["proto"])
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
tonic_prost_build::configure()
.file_descriptor_set_path(out_dir.join("track_descriptor.bin"))
.compile_protos(&["proto/track.proto"], &["proto"])
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
Ok(())
}

View File

@@ -1,49 +0,0 @@
syntax = "proto3";
package termix.audio_backend;
import "google/protobuf/empty.proto";
service AudioBackendRPC {
// Stop the active audio server
rpc StopClient(google.protobuf.Empty) returns (google.protobuf.Empty);
// Start the audio server of choice
rpc StartClient(DesiredAudioBacked) returns (google.protobuf.Empty);
// Get information about the possible audio backend configuration options
rpc DescribeBackend(DesiredAudioBacked) returns (AudioBackendDescription);
rpc InitConnection(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc GetAvailableBackends(google.protobuf.Empty) returns (BackendList);
}
enum SupportedAudioBackends {
AB_UNSPECIFIED = 0;
AB_JACK = 1;
AB_COREAUDIO = 2;
}
message DesiredAudioBacked {
SupportedAudioBackends backend = 1;
CoreAudioOptions core_audio_opts = 2;
}
message AudioBackendDescription {
CoreAudioAvailableOptions core_audio_description = 1;
repeated string input_devices = 2;
repeated string output_devices = 3;
}
message CoreAudioAvailableOptions {
repeated string input_devices = 1;
repeated string output_devices = 2;
}
message CoreAudioOptions {
string input_device = 1;
}
message BackendList {
repeated Backend backends = 1;
}
message Backend {
string name = 1;
}

View File

@@ -1,19 +0,0 @@
syntax = "proto3";
package termix.track;
import "google/protobuf/empty.proto";
service TrackOp {
rpc Create(Track) returns (google.protobuf.Empty);
rpc List(google.protobuf.Empty) returns (Tracks);
}
message Track {
string name = 1;
}
message Tracks {
repeated Track tracks = 1;
}

View File

@@ -1,7 +0,0 @@
pub mod termix {
pub mod audio_backend {
pub const FILE_DESCRIPTOR_SET: &[u8] =
tonic::include_file_descriptor_set!("audio_backend_descriptor");
tonic::include_proto!("termix.audio_backend");
}
}

View File

@@ -1,25 +0,0 @@
use uuid::Uuid;
pub struct Metadata {
id: Uuid,
name: String,
}
impl Metadata {
pub fn new(name: String) -> Self {
let id = Uuid::new_v4();
Self { id, name }
}
pub fn id(&self) -> Uuid {
self.id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn set_name(&mut self, name: String) {
self.name = name;
}
}

View File

@@ -1,9 +0,0 @@
use crate::{metadata::Metadata, region::Region, track::Track};
pub struct Project {
pub name: String,
pub tracks: Option<Vec<Track>>,
pub regions: Option<Vec<Region>>,
// Current playhead position
pub current_sample: u64,
}

View File

@@ -1,11 +0,0 @@
use crate::metadata::Metadata;
pub struct Region {
pub metadata: Metadata,
// Position of the track on the track
pub starts_at: u64,
// From which point of the audio source the region starts
pub plays_from: u64,
// Duration of the region after plays_from
pub duration: u64,
}

View File

@@ -1,14 +0,0 @@
use crate::metadata::Metadata;
pub enum TrackType {
Audio,
Midi,
}
pub struct Track {
pub metadata: Metadata,
pub track_type: TrackType,
pub active: bool,
}
pub struct TrackStatus {}

BIN
resources/audio/session.flac (Stored with Git LFS)

Binary file not shown.

BIN
resources/audio/session.mp3 (Stored with Git LFS)

Binary file not shown.

BIN
resources/audio/session.wav (Stored with Git LFS)

Binary file not shown.

122
src/main.rs Normal file
View File

@@ -0,0 +1,122 @@
use std::{fs::File, path::PathBuf};
use clap::{Parser, Subcommand, ValueEnum};
use symphonia::core::{audio::SampleBuffer, codecs::DecoderOptions, formats::FormatOptions, io::MediaSourceStream, meta::MetadataOptions, probe::Hint};
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum Backend {
Jack,
Alsa,
Pulseaudio,
Coreaudio,
}
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
name: Option<String>,
#[arg(short, long, value_enum)]
backend: Option<Backend>,
#[arg(short, long)]
track: Option<Vec<String>>,
}
fn main() {
let cli = Cli::parse();
// You can check the value provided by positional arguments, or option arguments
// Read the track from the tracks into the buffer
if let Some(files) = cli.track {
for file in files {
read_audio_file(file);
}
}
if let Some(backend) = cli.backend {
match backend {
Backend::Jack => println!("jack"),
Backend::Alsa => println!("alsa"),
Backend::Pulseaudio => println!("pulse"),
Backend::Coreaudio => println!("coreaudio"),
}
}
}
fn read_audio_file(file: String) {
let file = Box::new(File::open(file).unwrap());
let mss = MediaSourceStream::new(file, Default::default());
let mut hint = Hint::new();
hint.with_extension("mp3");
let format_opts: FormatOptions = Default::default();
let metadata_opts: MetadataOptions = Default::default();
let decoder_opts: DecoderOptions = Default::default();
let probed =
symphonia::default::get_probe().format(&hint, mss, &format_opts, &metadata_opts).unwrap();
// Get the format reader yielded by the probe operation.
let mut format = probed.format;
// Get the default track.
let track = format.default_track().unwrap();
// Create a decoder for the track.
let mut decoder =
symphonia::default::get_codecs().make(&track.codec_params, &decoder_opts).unwrap();
// Store the track identifier, we'll use it to filter packets.
let track_id = track.id;
let mut sample_count = 0;
let mut sample_buf = None;
loop {
// Get the next packet from the format reader.
let packet = format.next_packet().unwrap();
// If the packet does not belong to the selected track, skip it.
if packet.track_id() != track_id {
continue;
}
// Decode the packet into audio samples, ignoring any decode errors.
match decoder.decode(&packet) {
Ok(audio_buf) => {
// The decoded audio samples may now be accessed via the audio buffer if per-channel
// slices of samples in their native decoded format is desired. Use-cases where
// the samples need to be accessed in an interleaved order or converted into
// another sample format, or a byte buffer is required, are covered by copying the
// audio buffer into a sample buffer or raw sample buffer, respectively. In the
// example below, we will copy the audio buffer into a sample buffer in an
// interleaved order while also converting to a f32 sample format.
// If this is the *first* decoded packet, create a sample buffer matching the
// decoded audio buffer format.
if sample_buf.is_none() {
// Get the audio buffer specification.
let spec = *audio_buf.spec();
// Get the capacity of the decoded buffer. Note: This is capacity, not length!
let duration = audio_buf.capacity() as u64;
// Create the f32 sample buffer.
sample_buf = Some(SampleBuffer::<f32>::new(duration, spec));
}
// Copy the decoded audio buffer into the sample buffer in an interleaved format.
if let Some(buf) = &mut sample_buf {
buf.copy_interleaved_ref(audio_buf);
// The samples may now be access via the `samples()` function.
sample_count += buf.samples().len();
print!("\rDecoded {} samples", sample_count);
}
}
Err(symphonia::core::errors::Error::DecodeError(_)) => (),
Err(_) => break,
}
}
}
pub struct DecodedAudio {
pub samples: Vec<i16>, // interleaved
pub sample_rate: u32,
pub channels: u16,
}

View File

@@ -1,10 +0,0 @@
[package]
name = "tui"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4.5.53", features = ["derive"] }
color-eyre = "0.6.5"
crossterm = "0.29.0"
ratatui = "0.29.0"

View File

@@ -1,31 +0,0 @@
use clap::Parser;
use color_eyre::Result;
use crossterm::event::{self, Event};
use ratatui::{DefaultTerminal, Frame};
/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {}
fn main() -> Result<()> {
let _ = Args::parse();
color_eyre::install()?;
let terminal = ratatui::init();
let result = run(terminal);
ratatui::restore();
result
}
fn run(mut terminal: DefaultTerminal) -> Result<()> {
loop {
terminal.draw(render)?;
if matches!(event::read()?, Event::Key(_)) {
break Ok(());
}
}
}
fn render(frame: &mut Frame) {
frame.render_widget("hello world", frame.area());
}