Skip to content

Commit

Permalink
loungy CLI initial
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthiasGrandl committed Jun 21, 2024
1 parent 7cba833 commit d3c7544
Show file tree
Hide file tree
Showing 17 changed files with 235 additions and 16 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ crossbeam-channel = "0.5.12"
reqwest = "0.11.24"
quick-xml = "0.31.0"
scraper = "0.19.0"
clap = { version = "4.5.7", features = ["cargo", "derive", "string"] }

[target.'cfg(target_os = "macos")'.dependencies]
swift-rs = "1.0.6"
Expand Down
5 changes: 4 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@
*
*/

use async_std::os::unix::net::UnixListener;
use gpui::*;

use crate::{
assets::Assets,
commands::RootCommands,
hotkey::HotkeyManager,
ipc::server::start_server,
theme::Theme,
window::{Window, WindowStyle},
workspace::Workspace,
};

pub fn run_app(app: gpui::App) {
pub fn run_app(listener: UnixListener, app: gpui::App) {
app.with_assets(Assets).run(move |cx: &mut AppContext| {
Theme::init(cx);
// TODO: This still only works for a single display
Expand All @@ -37,6 +39,7 @@ pub fn run_app(app: gpui::App) {
theme.window_background.clone().unwrap_or_default(),
));
RootCommands::init(cx);
cx.spawn(|cx| start_server(listener, cx)).detach();
HotkeyManager::init(cx);
let view = Workspace::build(cx);
Window::init(cx);
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions src/commands/menu/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod list;
21 changes: 13 additions & 8 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use std::collections::HashMap;

use gpui::*;
use log::error;
use serde::{Deserialize, Serialize};

use crate::{
command,
Expand All @@ -27,28 +28,32 @@ use crate::{
},
};

#[cfg(target_os = "macos")]
use self::root::menu;
use self::root::{list, process, theme};
use self::root::list;

#[cfg(feature = "bitwarden")]
mod bitwarden;
#[cfg(feature = "clipboard")]
mod clipboard;
#[cfg(feature = "matrix")]
mod matrix;
#[cfg(target_os = "macos")]
mod menu;
mod process;
pub mod root;
#[cfg(feature = "tailscale")]
mod tailscale;
mod theme;

#[derive(Clone)]
#[derive(Clone, Serialize, Deserialize)]
pub struct RootCommand {
pub id: String,
title: String,
subtitle: String,
icon: Icon,
keywords: Vec<String>,
#[serde(skip)]
shortcut: Option<Shortcut>,
#[serde(skip)]
pub action: Box<dyn CloneableFn>,
}
impl RootCommand {
Expand Down Expand Up @@ -77,7 +82,7 @@ pub trait RootCommandBuilder: CommandTrait {
fn build(&self, cx: &mut WindowContext) -> RootCommand;
}

#[derive(Clone)]
#[derive(Clone, Serialize, Deserialize)]
pub struct RootCommands {
pub commands: HashMap<String, RootCommand>,
}
Expand All @@ -87,9 +92,9 @@ impl RootCommands {
let commands: Vec<Box<dyn RootCommandBuilder>> = vec![
Box::new(list::LoungyCommandBuilder),
#[cfg(target_os = "macos")]
Box::new(menu::MenuCommandBuilder),
Box::new(process::ProcessCommandBuilder),
Box::new(theme::ThemeCommandBuilder),
Box::new(menu::list::MenuCommandBuilder),
Box::new(process::list::ProcessCommandBuilder),
Box::new(theme::list::ThemeCommandBuilder),
#[cfg(feature = "tailscale")]
Box::new(tailscale::list::TailscaleCommandBuilder),
#[cfg(feature = "bitwarden")]
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions src/commands/process/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod list;
4 changes: 0 additions & 4 deletions src/commands/root/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,4 @@
*/

pub mod list;
#[cfg(target_os = "macos")]
pub mod menu;
pub mod numbat;
pub mod process;
pub mod theme;
File renamed without changes.
1 change: 1 addition & 0 deletions src/commands/theme/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod list;
3 changes: 2 additions & 1 deletion src/components/shared/icon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use std::fmt;

use gpui::SharedString;
use serde::{Deserialize, Serialize};

fn to_kebap(s: &str) -> String {
s.chars().fold(String::new(), |mut s, c| {
Expand Down Expand Up @@ -40,7 +41,7 @@ impl fmt::Display for Icon {
}
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]

pub enum Icon {
Ratio,
Expand Down
35 changes: 35 additions & 0 deletions src/ipc/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use async_std::{io::ReadExt, io::WriteExt, os::unix::net::UnixStream};
use clap::Command;

use crate::commands::RootCommands;

use super::{
server::{get_command, CommandPayload, TopLevelCommand},
SOCKET_PATH,
};

pub async fn client_connect() -> anyhow::Result<()> {
let mut stream = UnixStream::connect(SOCKET_PATH).await?;

let mut buf = vec![0; 8096];
let n = stream.read(&mut buf).await?;
let root_commands: RootCommands = serde_json::from_slice(&buf[..n])?;

let command: Command = get_command(&root_commands);

let matches = command.get_matches();

let payload: CommandPayload = CommandPayload {
action: matches
.get_one::<TopLevelCommand>("Action")
.ok_or(anyhow::anyhow!("Action not found"))?
.clone(),
command: matches.get_one::<String>("Command").cloned(),
};

let bytes = serde_json::to_vec(&payload)?;

stream.write_all(&bytes).await?;

Ok(())
}
4 changes: 4 additions & 0 deletions src/ipc/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod client;
pub mod server;

pub const SOCKET_PATH: &str = "/tmp/loungy.sock";
157 changes: 157 additions & 0 deletions src/ipc/server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
use std::path::Path;

use anyhow::anyhow;
use async_std::{
io::{ReadExt, WriteExt},
os::unix::net::{UnixListener, UnixStream},
};
use clap::{command, Arg, ValueEnum};
use gpui::AsyncWindowContext;
use serde::{Deserialize, Serialize};

use crate::{
commands::RootCommands,
state::{Actions, StateModel},
window::Window,
};

use super::SOCKET_PATH;

pub async fn setup_socket() -> anyhow::Result<UnixListener> {
if Path::new(SOCKET_PATH).exists() {
if UnixStream::connect(SOCKET_PATH).await.is_ok() {
return Err(anyhow!("Server already running"));
}
std::fs::remove_file(SOCKET_PATH)?;
};
let listener = UnixListener::bind(SOCKET_PATH).await?;
log::info!("Listening on socket: {}", SOCKET_PATH);

Ok(listener)
}

pub async fn start_server(
listener: UnixListener,
mut cx: AsyncWindowContext,
) -> anyhow::Result<()> {
let commands = cx.read_global::<RootCommands, _>(|commands, _| commands.clone())?;
loop {
let (stream, _) = listener.accept().await?;
cx.spawn(|cx| handle_client(stream, commands.clone(), cx))
.detach();
}
}

async fn handle_client(
mut stream: UnixStream,
commands: RootCommands,
mut cx: AsyncWindowContext,
) -> anyhow::Result<()> {
// Send available commands to the client
let bytes = serde_json::to_vec(&commands)?;
stream.write_all(&bytes).await?;

let mut buf = vec![0; 1024];
let n = stream.read(&mut buf).await?;

let matches: CommandPayload = serde_json::from_slice(&buf[..n])?;

let _ = cx.update::<anyhow::Result<()>>(|cx| {
match matches.action {
TopLevelCommand::Toggle => {
Window::toggle(cx);
}
TopLevelCommand::Show => {
Window::open(cx);
}
TopLevelCommand::Hide => {
Window::close(cx);
}
TopLevelCommand::Quit => {
cx.quit();
}
TopLevelCommand::Command => {
let Some(c) = matches.command else {
return Err(anyhow!("No command provided"));
};
let Some((_, command)) = commands.commands.iter().find(|(k, _)| {
let split = k.split("::").collect::<Vec<_>>();
c.eq(split[2])
}) else {
return Err(anyhow!("Command not found"));
};

let state = cx.global::<StateModel>();
let state = state.inner.read(cx);
let mut is_active = false;
if let Some(active) = state.stack.last() {
is_active = active.id.eq(&command.id);
};
if !is_active {
StateModel::update(
|this, cx| {
this.reset(cx);
},
cx,
);
(command.action)(&mut Actions::default(cx), cx);
Window::open(cx);
} else {
Window::toggle(cx);
}
}
}
Ok(())
});
Ok(())
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CommandPayload {
pub action: TopLevelCommand,
pub command: Option<String>,
}

#[derive(Clone, Debug, ValueEnum, Serialize, Deserialize)]
pub enum TopLevelCommand {
Toggle,
Show,
Hide,
Quit,
Command,
}

impl From<TopLevelCommand> for clap::builder::OsStr {
fn from(cmd: TopLevelCommand) -> Self {
match cmd {
TopLevelCommand::Toggle => "toggle".into(),
TopLevelCommand::Show => "show".into(),
TopLevelCommand::Hide => "hide".into(),
TopLevelCommand::Quit => "quit".into(),
TopLevelCommand::Command => "command".into(),
}
}
}

pub fn get_command(commands: &RootCommands) -> clap::Command {
command!()
.arg(
Arg::new("Action")
.value_parser(clap::builder::EnumValueParser::<TopLevelCommand>::new())
.required(true),
)
.arg(
Arg::new("Command")
.required_if_eq("Action", TopLevelCommand::Command)
.value_parser(
commands
.commands
.keys()
.map(|key| {
let split = key.split("::").collect::<Vec<_>>();
split[2].to_string()
})
.collect::<Vec<_>>(),
),
)
}
10 changes: 8 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use app::run_app;
use gpui::App;
use ipc::{client::client_connect, server::setup_socket};

mod app;
mod assets;
Expand All @@ -21,6 +22,7 @@ mod components;
mod date;
mod db;
mod hotkey;
mod ipc;
mod loader;
mod paths;
mod platform;
Expand All @@ -33,7 +35,11 @@ mod workspace;
#[async_std::main]
async fn main() {
env_logger::init();
let app = App::new();

run_app(app)
if let Ok(listener) = setup_socket().await {
let app = App::new();
run_app(listener, app);
} else if let Err(e) = client_connect().await {
log::error!("CLI Error: {:?}", e);
}
}
7 changes: 7 additions & 0 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,13 @@ impl<'a> Clone for Box<dyn 'a + CloneableFn> {
}
}

// implement Default for CloneableFn
impl Default for Box<dyn CloneableFn> {
fn default() -> Self {
Box::new(|_, _| {})
}
}

#[derive(Clone, IntoElement, Deserialize)]
pub struct Shortcut {
inner: Keystroke,
Expand Down

0 comments on commit d3c7544

Please sign in to comment.