First working* version after rewrite
This commit is contained in:
parent
18271ac1d3
commit
5df9833edd
22 changed files with 2768 additions and 396 deletions
266
src/clients.cpp
266
src/clients.cpp
|
@ -1,266 +0,0 @@
|
|||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "clients.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
json workspacesOutput;
|
||||
std::map<std::string, std::string> iconMap;
|
||||
std::map<int, std::string> monitorMap;
|
||||
|
||||
std::map<std::string, int> specialWorkspaceMap = {{"special:ctrl", 0},
|
||||
{"special:alt", 1},
|
||||
{"special:altgr", 2},
|
||||
{"special:strg", 3}};
|
||||
|
||||
/*json workspacesOutput = json::parse(R"(
|
||||
[
|
||||
"normal":
|
||||
{
|
||||
"activeOn": str
|
||||
"icon": str
|
||||
"id": int
|
||||
"occupied": int
|
||||
},
|
||||
"special":
|
||||
{
|
||||
"activeOn": str
|
||||
"icon": str
|
||||
"id": str
|
||||
"occupied": int
|
||||
}
|
||||
]
|
||||
|
||||
)");*/
|
||||
|
||||
std::string command(std::string inputCommand) {
|
||||
const char *command = inputCommand.c_str();
|
||||
char buffer[128];
|
||||
std::string result;
|
||||
FILE *pipe = popen(command, "r");
|
||||
|
||||
if (!pipe) {
|
||||
std::cerr << "No pipe opened" << std::endl;
|
||||
return "";
|
||||
}
|
||||
|
||||
while (!feof(pipe)) {
|
||||
if (fgets(buffer, 128, pipe) != NULL) {
|
||||
result += buffer;
|
||||
}
|
||||
}
|
||||
|
||||
pclose(pipe);
|
||||
return result;
|
||||
}
|
||||
|
||||
void generateIconMap() {
|
||||
json clients = json::parse(command("hyprctl clients -j"));
|
||||
|
||||
for (const json &client : clients) {
|
||||
std::string pid = std::to_string(static_cast<int>(client["pid"]));
|
||||
|
||||
if (pid == "-1") {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string initClass = client["initialClass"];
|
||||
if (initClass == "") {
|
||||
initClass = "aguiienagi";
|
||||
}
|
||||
|
||||
// TODO unjank
|
||||
|
||||
std::string cmd = "cd /home/willifan/.config/eww/scripts/ && ./test.sh ";
|
||||
std::string test =
|
||||
std::string("cd /home/willifan/.config/desktop-utils/ewwScripts && "
|
||||
"./test.sh ") +
|
||||
initClass + " " + pid;
|
||||
std::cout << test << std::endl;
|
||||
|
||||
iconMap[client["address"]] = command(test);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void generateMonitorMap() {
|
||||
json monitorsInput = json::parse(command("hyprctl monitors -j"));
|
||||
|
||||
monitorMap.clear();
|
||||
|
||||
for (const json &monitor : monitorsInput) {
|
||||
monitorMap[monitor["activeWorkspace"]["id"]] =
|
||||
std::to_string(static_cast<int>(monitor["id"]));
|
||||
}
|
||||
}
|
||||
|
||||
json getWorkspace(json workspaceInput) {
|
||||
json workspaceOutput;
|
||||
|
||||
if (workspaceInput["id"] > 0) // normal workspace
|
||||
{
|
||||
workspaceOutput["id"] = workspaceInput["id"];
|
||||
} else // special workspace
|
||||
{
|
||||
workspaceOutput["id"] = workspaceInput["name"];
|
||||
}
|
||||
|
||||
workspaceOutput["activeOn"] = monitorMap[workspaceInput["id"]];
|
||||
workspaceOutput["occupied"] = workspaceInput["windows"];
|
||||
|
||||
if (!(workspaceInput["lastwindow"] == "0x0")) // workspace not empty
|
||||
{
|
||||
workspaceOutput["icon"] = iconMap[workspaceInput["lastwindow"]];
|
||||
} else // workspace empty
|
||||
{
|
||||
workspaceOutput["icon"] = "";
|
||||
}
|
||||
|
||||
return workspaceOutput;
|
||||
}
|
||||
|
||||
// TODO fix special workspaces
|
||||
void getAllWorkspaces() {
|
||||
json workspacesInput = json::parse(command("hyprctl workspaces -j"));
|
||||
|
||||
int specialIndex = 4; // next index after specialWorkspaceMap
|
||||
|
||||
for (const auto &workspace : workspacesInput) {
|
||||
if (workspace["id"] >= 1 && workspace["id"] <= 9) {
|
||||
int index = workspace["id"].get<int>() - 1;
|
||||
|
||||
workspacesOutput[index]["normal"].clear();
|
||||
workspacesOutput[index]["normal"] = getWorkspace(workspace);
|
||||
} else if (std::string(workspace["name"]).find("special:") == 0) {
|
||||
std::string name = workspace["name"];
|
||||
|
||||
int currentIndex;
|
||||
|
||||
if (specialWorkspaceMap.contains(name)) {
|
||||
currentIndex = specialWorkspaceMap[name];
|
||||
} else {
|
||||
currentIndex = specialIndex;
|
||||
specialIndex++;
|
||||
}
|
||||
workspacesOutput[currentIndex]["special"].clear();
|
||||
workspacesOutput[currentIndex]["special"] = getWorkspace(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO optimize whatever
|
||||
|
||||
void handle(std::string message) {
|
||||
/*
|
||||
Call generateMonitorMap when workspaces change their monitor
|
||||
Call generateIconMap when clients are opened or closed
|
||||
*/
|
||||
|
||||
if (message.find("workspacev2>>") ==
|
||||
0) // emitted ONCE when switching to a workspace
|
||||
{
|
||||
generateMonitorMap();
|
||||
getAllWorkspaces();
|
||||
std::cout << workspacesOutput << std::endl;
|
||||
} else if (message.find("moveworkspacev2>>") ==
|
||||
0) // emitted when a workspace switches to another monitor, TWICE
|
||||
// when swaping
|
||||
{
|
||||
generateMonitorMap();
|
||||
getAllWorkspaces();
|
||||
std::cout << workspacesOutput << std::endl;
|
||||
} else if (message.find("openwindow>>") ==
|
||||
0) // emitted ONCE when a new client is created
|
||||
{
|
||||
generateIconMap();
|
||||
getAllWorkspaces();
|
||||
std::cout << workspacesOutput << std::endl;
|
||||
} else if (message.find("closewindow>>") ==
|
||||
0) // emitted ONCE when a client gets closed
|
||||
{
|
||||
generateIconMap();
|
||||
getAllWorkspaces();
|
||||
std::cout << workspacesOutput << std::endl;
|
||||
} else if (message.find("movewindowv2>>") ==
|
||||
0) // emitted ONCE when a client changes its workspace
|
||||
{
|
||||
getAllWorkspaces();
|
||||
std::cout << workspacesOutput << std::endl;
|
||||
} else if (message.find("activewindow>>v2") ==
|
||||
0) // emitted ONCE when focus changes to another client
|
||||
{
|
||||
getAllWorkspaces();
|
||||
std::cout << workspacesOutput << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
int clients::clients() {
|
||||
generateIconMap();
|
||||
generateMonitorMap();
|
||||
getAllWorkspaces();
|
||||
std::cout << workspacesOutput << std::endl;
|
||||
|
||||
std::string socketPath =
|
||||
std::string(std::getenv("XDG_RUNTIME_DIR")) + "/hypr/" +
|
||||
std::string(std::getenv("HYPRLAND_INSTANCE_SIGNATURE")) +
|
||||
"/.socket2.sock";
|
||||
|
||||
// Create a socket
|
||||
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (sockfd == -1) {
|
||||
std::cerr << "Error: Failed to create socket\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Define the address of the IPC socket
|
||||
struct sockaddr_un addr;
|
||||
addr.sun_family = AF_UNIX;
|
||||
strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1);
|
||||
|
||||
// Connect to the IPC socket
|
||||
if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
|
||||
std::cerr << "Error: Failed to connect to IPC socket\n";
|
||||
close(sockfd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Receive and print messages from the IPC socket
|
||||
char buffer[1024];
|
||||
ssize_t bytes_received;
|
||||
while ((bytes_received = recv(sockfd, buffer, sizeof(buffer), 0)) > 0) {
|
||||
|
||||
std::string message = std::string(buffer, bytes_received);
|
||||
|
||||
std::istringstream iss(message);
|
||||
std::string messageLine;
|
||||
|
||||
while (std::getline(iss, messageLine)) {
|
||||
handle(messageLine);
|
||||
}
|
||||
}
|
||||
if (bytes_received == -1) {
|
||||
std::cerr << "Error: Failed to receive message\n";
|
||||
}
|
||||
|
||||
// Close the socket
|
||||
close(sockfd);
|
||||
|
||||
return 0;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
namespace clients {
|
||||
int clients();
|
||||
}
|
151
src/hyprland.rs
Normal file
151
src/hyprland.rs
Normal file
|
@ -0,0 +1,151 @@
|
|||
//use serde::{Deserialize, Serialize};
|
||||
//use serde_json::{self, json};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::process::Command;
|
||||
use std::str;
|
||||
use std::string::String;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
const DESKTOP_PATH: &str = "/run/current-system/sw/share/applications/";
|
||||
const ENV_INSTANCE_SIGNATURE: &str = "HYPRLAND_INSTANCE_SIGNATURE";
|
||||
|
||||
//struct Workspace {
|
||||
// id: i32,
|
||||
// active_on: u32,
|
||||
// num_windows: u32,
|
||||
// icon: String,
|
||||
//}
|
||||
|
||||
//fn fetch_workspaces() {}
|
||||
|
||||
fn fetch_monitors() {
|
||||
let monitors = Command::new("hyprctl monitors").arg("-j").output();
|
||||
let monitors = match monitors {
|
||||
Ok(monitor) => monitor.stdout,
|
||||
Err(_error) => b"Monitor error".to_vec(),
|
||||
};
|
||||
}
|
||||
|
||||
//fn fetch_clients() {}
|
||||
|
||||
fn generate_icon_map() -> HashMap<String, String> {
|
||||
let files = fs::read_dir(DESKTOP_PATH).unwrap();
|
||||
|
||||
let mut vector = vec![];
|
||||
|
||||
let mut i = 0;
|
||||
|
||||
for file in files {
|
||||
let file = match file {
|
||||
Ok(file) => file,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let path = file.path();
|
||||
let extension = path.extension().expect("error");
|
||||
|
||||
if extension != "desktop" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut class = String::new();
|
||||
let mut icon = String::new();
|
||||
|
||||
let file = fs::read_to_string(path).unwrap();
|
||||
|
||||
let mut got_icon = false;
|
||||
let mut got_class = false;
|
||||
|
||||
for line in file.lines() {
|
||||
let line = line.replace(' ', "");
|
||||
match (got_icon, &line) {
|
||||
(true, _) => (),
|
||||
(false, line) if line.starts_with("Icon=") => {
|
||||
icon = line.split_once('=').expect("error").1.to_string();
|
||||
got_icon = true;
|
||||
if got_class {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match (got_class, &line) {
|
||||
(true, _) => (),
|
||||
(false, line) if line.starts_with("StartupWMClass=") => {
|
||||
class = line.split_once('=').expect("error").1.to_string();
|
||||
got_class = true;
|
||||
if got_icon {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
(false, line) if line.starts_with("Name=") => {
|
||||
class = line.split_once('=').expect("error").1.to_string();
|
||||
continue;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
vector.push((class, icon));
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let mut icon_map: HashMap<String, String> = HashMap::with_capacity(i);
|
||||
icon_map.extend(vector);
|
||||
|
||||
for (key, value) in &icon_map {
|
||||
println!("Key: {}, Value: {}", key, value);
|
||||
}
|
||||
|
||||
icon_map
|
||||
}
|
||||
|
||||
fn handle_workspace() {
|
||||
fetch_monitors();
|
||||
}
|
||||
fn handle_moveworkspace() {}
|
||||
fn handle_activespecial() {}
|
||||
fn handle_openwindow() {}
|
||||
fn handle_closewindow() {}
|
||||
fn handle_movewindow() {}
|
||||
fn handle_activewindow() {}
|
||||
|
||||
fn handle(message: &str) {
|
||||
println!("{}", message);
|
||||
match message {
|
||||
s if s.starts_with("workspacev2>>") => handle_workspace(),
|
||||
s if s.starts_with("moveworkspacev2>>") => handle_moveworkspace(),
|
||||
s if s.starts_with("activespecial>>") => handle_activespecial(),
|
||||
s if s.starts_with("openwindow>>") => handle_openwindow(),
|
||||
s if s.starts_with("closewindow>>") => handle_closewindow(),
|
||||
s if s.starts_with("movewindowv2>>") => handle_movewindow(),
|
||||
s if s.starts_with("activewindow>>v2") => handle_activewindow(),
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let icon_map = generate_icon_map();
|
||||
|
||||
let hyprland_instance_signature = env::var(ENV_INSTANCE_SIGNATURE).unwrap();
|
||||
let address = format!(
|
||||
"/run/user/1000/hypr/{}/.socket2.sock",
|
||||
hyprland_instance_signature
|
||||
);
|
||||
let stream = UnixStream::connect(address).unwrap();
|
||||
let mut reader = BufReader::new(stream);
|
||||
|
||||
loop {
|
||||
let mut buf = String::new();
|
||||
reader.read_line(&mut buf).unwrap();
|
||||
handle(buf.trim());
|
||||
sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
26
src/main.cpp
26
src/main.cpp
|
@ -1,26 +0,0 @@
|
|||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "clients.hpp"
|
||||
|
||||
std::vector<std::string> splitArgs(int argc, char **argv) {
|
||||
|
||||
std::vector<std::string> result;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
result.push_back(argv[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
|
||||
const auto ARGS = splitArgs(argc, argv);
|
||||
|
||||
for (std::string arg : ARGS) {
|
||||
if (arg == "clients")
|
||||
clients::clients();
|
||||
}
|
||||
return 0;
|
||||
}
|
158
src/main.rs
Normal file
158
src/main.rs
Normal file
|
@ -0,0 +1,158 @@
|
|||
//! Test application to list all available outputs.
|
||||
|
||||
use std::error::Error;
|
||||
use std::process::Command;
|
||||
|
||||
use smithay_client_toolkit::{
|
||||
delegate_output, delegate_registry,
|
||||
output::{OutputHandler, OutputState},
|
||||
registry::{ProvidesRegistryState, RegistryState},
|
||||
registry_handlers,
|
||||
};
|
||||
|
||||
use wayland_client::{globals::registry_queue_init, protocol::wl_output, Connection, QueueHandle};
|
||||
|
||||
const CONFIG_DIR: &str = "/home/willifan/.config/desktop-utils/eww";
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// We initialize the logger for the purpose of debugging.
|
||||
// Set `RUST_LOG=debug` to see extra debug information.
|
||||
env_logger::init();
|
||||
|
||||
// Try to connect to the Wayland server.
|
||||
let conn = Connection::connect_to_env()?;
|
||||
|
||||
// Now create an event queue and a handle to the queue so we can create objects.
|
||||
let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
|
||||
let qh = event_queue.handle();
|
||||
|
||||
// Initialize the registry handling so other parts of Smithay's client toolkit may bind
|
||||
// globals.
|
||||
let registry_state = RegistryState::new(&globals);
|
||||
|
||||
// Initialize the delegate we will use for outputs.
|
||||
let output_delegate = OutputState::new(&globals, &qh);
|
||||
|
||||
// Set up application state.
|
||||
//
|
||||
// This is where you will store your delegates and any data you wish to access/mutate while the
|
||||
// application is running.
|
||||
let mut list_outputs = ListOutputs {
|
||||
registry_state,
|
||||
output_state: output_delegate,
|
||||
};
|
||||
|
||||
// `OutputState::new()` binds the output globals found in `registry_queue_init()`.
|
||||
//
|
||||
// After the globals are bound, we need to dispatch again so that events may be sent to the newly
|
||||
// created objects.
|
||||
event_queue.roundtrip(&mut list_outputs)?;
|
||||
|
||||
// Now our outputs have been initialized with data, we may access what outputs exist and information about
|
||||
// said outputs using the output delegate.
|
||||
for output in list_outputs.output_state.outputs() {
|
||||
if let Some((width, _height)) = &list_outputs
|
||||
.output_state
|
||||
.info(&output)
|
||||
.ok_or_else(|| "output has no info".to_owned())?
|
||||
.logical_size
|
||||
.as_ref()
|
||||
{
|
||||
println!("Opening eww with width {}", width);
|
||||
let mut child = Command::new("eww")
|
||||
.args([
|
||||
"open",
|
||||
"bar",
|
||||
"--id",
|
||||
"bar0", //TODO account for more monitors
|
||||
"--arg",
|
||||
"monitor=0", //TODO account for more monitors
|
||||
"--arg",
|
||||
&format!("width={width}"),
|
||||
"--arg",
|
||||
"height=30",
|
||||
"--config",
|
||||
CONFIG_DIR, //TODO make this path implicit
|
||||
])
|
||||
.spawn()
|
||||
.expect("failed to execute process");
|
||||
child.wait().expect("failed to wait on child");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Application data.
|
||||
///
|
||||
/// This type is where the delegates for some parts of the protocol and any application specific data will
|
||||
/// live.
|
||||
struct ListOutputs {
|
||||
registry_state: RegistryState,
|
||||
output_state: OutputState,
|
||||
}
|
||||
|
||||
// In order to use OutputDelegate, we must implementIm ver this trait to indicate when something has happened to an
|
||||
// output and to provide an instance of the output state to the delegate when dispatching events.
|
||||
impl OutputHandler for ListOutputs {
|
||||
// First we need to provide a way to access the delegate.
|
||||
//
|
||||
// This is needed because delegate implementations for handling events use the application data type in
|
||||
// their function signatures. This allows the implementation to access an instance of the type.
|
||||
fn output_state(&mut self) -> &mut OutputState {
|
||||
&mut self.output_state
|
||||
}
|
||||
|
||||
// Then there exist these functions that indicate the lifecycle of an output.
|
||||
// These will be called as appropriate by the delegate implementation.
|
||||
|
||||
fn new_output(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
) {
|
||||
}
|
||||
|
||||
fn update_output(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
) {
|
||||
}
|
||||
|
||||
fn output_destroyed(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
// Now we need to say we are delegating the responsibility of output related events for our application data
|
||||
// type to the requisite delegate.
|
||||
delegate_output!(ListOutputs);
|
||||
|
||||
// In order for our delegate to know of the existence of globals, we need to implement registry
|
||||
// handling for the program. This trait will forward events to the RegistryHandler trait
|
||||
// implementations.
|
||||
delegate_registry!(ListOutputs);
|
||||
|
||||
// In order for delegate_registry to work, our application data type needs to provide a way for the
|
||||
// implementation to access the registry state.
|
||||
//
|
||||
// We also need to indicate which delegates will get told about globals being created. We specify
|
||||
// the types of the delegates inside the array.
|
||||
impl ProvidesRegistryState for ListOutputs {
|
||||
fn registry(&mut self) -> &mut RegistryState {
|
||||
&mut self.registry_state
|
||||
}
|
||||
|
||||
registry_handlers! {
|
||||
// Here we specify that OutputState needs to receive events regarding the creation and destruction of
|
||||
// globals.
|
||||
OutputState,
|
||||
}
|
||||
}
|
64
src/newtest.rs
Normal file
64
src/newtest.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use std::fs;
|
||||
use std::time::Instant;
|
||||
|
||||
const PATH: &str = "/usr/share/applications/";
|
||||
|
||||
fn read(path: &str) -> std::io::Result<()> {
|
||||
let files = fs::read_dir(path)?;
|
||||
for file in files {
|
||||
let file = file?;
|
||||
let path = file.path();
|
||||
let extension = path.extension().expect("error");
|
||||
|
||||
if extension != "desktop" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut class: &str = "";
|
||||
let mut name: &str = "";
|
||||
let mut icon: &str = "";
|
||||
|
||||
let file = fs::read_to_string(path)?;
|
||||
|
||||
for line in file.lines() {
|
||||
match line {
|
||||
line if line.starts_with("StartupWMClass") => {
|
||||
class = match line.split_once('=') {
|
||||
Some((_, class)) => class,
|
||||
None => "",
|
||||
}
|
||||
}
|
||||
line if line.starts_with("Name") => {
|
||||
name = match line.split_once('=') {
|
||||
Some((_, name)) => name,
|
||||
None => "",
|
||||
}
|
||||
}
|
||||
line if line.starts_with("Icon") => {
|
||||
icon = match line.split_once('=') {
|
||||
Some((_, icon)) => icon,
|
||||
None => "",
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
println!("Class: {class}, Name: {name}, Icon: {icon}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let start_time = Instant::now();
|
||||
read(PATH).expect("error");
|
||||
let end_time = Instant::now();
|
||||
|
||||
let time = end_time - start_time;
|
||||
|
||||
println!(
|
||||
"Time taken: {}.{:03}s",
|
||||
time.as_secs(),
|
||||
time.subsec_millis()
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue