Skip to content

Commit

Permalink
Move config into file
Browse files Browse the repository at this point in the history
Supporting live changes to the config is going to get increasingly complicated, and doesn't provide much value. The file is a necessary long-term solution anyway, for things like configurable theme and keybindings. Also deleted DrawContext since it's just one field now.

Closes #89
  • Loading branch information
LucasPickering committed Dec 21, 2023
1 parent 4ddb106 commit 71776cf
Show file tree
Hide file tree
Showing 29 changed files with 280 additions and 354 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@

## [Unreleased] - ReleaseDate

### Added

- Move app-level configuration into a file ([#89](https://github.com/LucasPickering/slumber/issues/89))
- Right now the only supported field is `preview_templates`

### Changed

- Show profile contents while in the profile list ([#26](https://github.com/LucasPickering/slumber/issues/26))
- Remove settings modal in favor of the settings file
- Supporting changing configuration values during a session adds a lot of complexity

## [0.11.0] - 2023-12-20

Expand Down
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

# API Reference

- [Configuration](./api/configuration.md)
- [Request Collection](./api/request_collection.md)
- [Profile](./api/profile.md)
- [Profile Value](./api/profile_value.md)
Expand Down
26 changes: 26 additions & 0 deletions docs/src/api/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Configuration

Configuration provides _application_-level settings, as opposed to collection-level settings.

## Location & Creation

Configuration is stored in the Slumber root directory, under the file `config.yml`. To find the root directory, you can run:

```sh
slumber show dir
```

To quickly create and edit the file:

```sh
# Replace vim with your favorite text editor
vim $(slumber show dir)/config.yml
```

If the root directory doesn't exist yet, you can create it yourself or have Slumber create it by simply starting the TUI.

## Fields

| Field | Type | Description | Default |
| ------------------- | --------- | ---------------------------------------------------------------------------- | ------- |
| `preview_templates` | `boolean` | Render template values in the TUI? If false, the raw template will be shown. | `true` |
2 changes: 1 addition & 1 deletion run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# latest watchexec we can get rid of this.
# https://github.com/watchexec/cargo-watch/issues/269

RUST_LOG=${RUST_LOG:-slumber=trace} watchexec --restart \
RUST_LOG=${RUST_LOG:-slumber=trace} watchexec --restart --no-process-group \
--watch Cargo.toml --watch Cargo.lock --watch src/ \
-- cargo run \
-- $@
33 changes: 13 additions & 20 deletions src/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
mod cereal;
mod insomnia;

use crate::template::Template;
use crate::{
template::Template,
util::{parse_yaml, ResultExt},
};
use anyhow::{anyhow, Context};
use derive_more::{Deref, Display, From};
use equivalent::Equivalent;
Expand All @@ -17,7 +20,6 @@ use std::{
hash::Hash,
path::{Path, PathBuf},
};
use tokio::fs;
use tracing::{info, warn};

/// The support file names to be automatically loaded as a config. We only
Expand Down Expand Up @@ -222,25 +224,16 @@ impl RequestCollection<PathBuf> {
/// [Self::detect_path] to find the file themself. This pattern enables the
/// TUI to start up and watch the collection file, even if it's invalid.
pub async fn load(path: PathBuf) -> anyhow::Result<Self> {
// Figure out which file we want to load from
info!(?path, "Loading collection file");

// First, parse the file to raw YAML values, so we can apply
// anchor/alias merging. Then parse that to our config type
let future = async {
let content = fs::read(&path).await?;
let mut yaml_value =
serde_yaml::from_slice::<serde_yaml::Value>(&content)?;
yaml_value.apply_merge()?;
Ok::<RequestCollection, anyhow::Error>(serde_yaml::from_value(
yaml_value,
)?)
};

Ok(future
.await
.context(format!("Error loading collection from {path:?}"))?
.with_source(path))
// This async block is really just a try block
let collection: RequestCollection = async {
let bytes = tokio::fs::read(&path).await?;
parse_yaml(&bytes).map_err(anyhow::Error::from)
}
.await
.context(format!("Error loading data from {path:?}"))
.traced()?;
Ok(collection.with_source(path))
}

/// Reload a new collection from the same file used for this one.
Expand Down
70 changes: 70 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use crate::util::{parse_yaml, Directory, ResultExt};
use anyhow::Context;
use serde::Deserialize;
use std::{
fs,
path::{Path, PathBuf},
};
use tracing::info;

/// App-level configuration, which is global across all sessions and
/// collections. This is *not* meant to modifiable during a session. If changes
/// are made to the config file while a session is running, they won't be
/// picked up until the app restarts.
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct Config {
/// The path that the config was loaded from, or tried to be loaded from if
/// the file didn't exist
#[serde(skip)]
path: PathBuf,
/// Should templates be rendered inline in the UI, or should we show the
/// raw text?
pub preview_templates: bool,
}

impl Config {
const FILE: &'static str = "config.yml";

/// Load configuration from the file, if present. If not, just return a
/// default value. This only returns an error if the file could be read, but
/// deserialization failed. This is *not* async because it's only run during
/// startup, when all operations are synchronous.
pub fn load() -> anyhow::Result<Self> {
let path = Directory::root().create()?.join(Self::FILE);
info!(?path, "Loading configuration file");

let mut config = match fs::read(&path) {
Ok(bytes) => parse_yaml::<Self>(&bytes)
.context(format!("Error loading configuration from {path:?}"))
.traced(),
// An error here is probably just the file missing, so don't make
// a big stink about it
Err(error) => {
info!(
?path,
error = &error as &dyn std::error::Error,
"Error reading configuration file"
);
Ok(Self::default())
}
}?;

config.path = path;
Ok(config)
}

/// The path where configuration is stored
pub fn path(&self) -> &Path {
&self.path
}
}

impl Default for Config {
fn default() -> Self {
Self {
path: PathBuf::default(),
preview_templates: true,
}
}
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

mod cli;
mod collection;
mod config;
mod db;
#[cfg(test)]
mod factory;
Expand Down
4 changes: 3 additions & 1 deletion src/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod view;

use crate::{
collection::{ProfileId, RequestCollection, RequestRecipeId},
config::Config,
db::{CollectionDatabase, Database},
http::{HttpEngine, RequestBuilder},
template::{Prompter, Template, TemplateChunk, TemplateContext},
Expand Down Expand Up @@ -78,13 +79,14 @@ impl Tui {
// ===== Initialize global state =====
// This stuff only needs to be set up *once per session*

let config = Config::load()?;
// Create a message queue for handling async tasks
let (messages_tx, messages_rx) = mpsc::unbounded_channel();
let messages_tx = MessageSender::new(messages_tx);
// Load a database for this particular collection
let database = Database::load()?.into_collection(&collection_file)?;
// Initialize global view context
TuiContext::init(messages_tx.clone(), database.clone());
TuiContext::init(config, messages_tx.clone(), database.clone());

// ===== Initialize collection & view =====

Expand Down
11 changes: 10 additions & 1 deletion src/tui/context.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{
config::Config,
db::CollectionDatabase,
tui::{
input::InputEngine,
Expand Down Expand Up @@ -28,6 +29,8 @@ static CONTEXT: OnceLock<TuiContext> = OnceLock::new();
/// Both are safe to access in statics!
#[derive(Debug)]
pub struct TuiContext {
/// App-level configuration
pub config: Config,
/// Visual theme. Colors!
pub theme: Theme,
/// Input:action bindings
Expand All @@ -41,9 +44,14 @@ pub struct TuiContext {

impl TuiContext {
/// Initialize global context. Should be called only once, during startup.
pub fn init(messages_tx: MessageSender, database: CollectionDatabase) {
pub fn init(
config: Config,
messages_tx: MessageSender,
database: CollectionDatabase,
) {
CONTEXT
.set(Self {
config,
theme: Theme::default(),
input_engine: InputEngine::default(),
messages_tx,
Expand Down Expand Up @@ -72,6 +80,7 @@ impl TuiContext {
pub fn tui_context() {
use tokio::sync::mpsc;
TuiContext::init(
Config::default(),
MessageSender::new(mpsc::unbounded_channel().0),
CollectionDatabase::testing(),
);
Expand Down
4 changes: 0 additions & 4 deletions src/tui/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ impl InputEngine {
)
.hide(),
InputBinding::new(KeyCode::Char('x'), Action::OpenActions),
InputBinding::new(KeyCode::Char('s'), Action::OpenSettings),
InputBinding::new(KeyCode::Char('?'), Action::OpenHelp),
InputBinding::new(KeyCode::Char('f'), Action::Fullscreen),
InputBinding::new(KeyCode::Char('r'), Action::ReloadCollection),
Expand Down Expand Up @@ -177,9 +176,6 @@ pub enum Action {
/// Open the actions modal
#[display("Actions")]
OpenActions,
/// Open the settings modal
#[display("Settings")]
OpenSettings,
#[display("Help")]
/// Open the help modal
OpenHelp,
Expand Down
32 changes: 3 additions & 29 deletions src/tui/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::{
input::Action,
view::{
component::{Component, Root},
draw::{Draw, DrawContext},
draw::Draw,
event::{Event, EventHandler, Update, UpdateContext},
state::Notification,
},
Expand All @@ -34,14 +34,12 @@ use tracing::{error, trace, trace_span};
/// controller and exposed via event passing.
#[derive(Debug)]
pub struct View {
config: ViewConfig,
root: Component<Root>,
}

impl View {
pub fn new(collection: &RequestCollection) -> Self {
let mut view = Self {
config: ViewConfig::default(),
root: Root::new(collection).into(),
};
// Tell the components to wake up
Expand All @@ -53,14 +51,7 @@ impl View {
/// to render input bindings as help messages to the user.
pub fn draw<'a>(&'a self, frame: &'a mut Frame) {
let chunk = frame.size();
self.root.draw(
&mut DrawContext {
config: &self.config,
frame,
},
(),
chunk,
)
self.root.draw(frame, (), chunk)
}

/// Update the request state for the given profile+recipe. The state will
Expand Down Expand Up @@ -126,8 +117,7 @@ impl View {

let span = trace_span!("View event", ?event);
span.in_scope(|| {
let mut context =
UpdateContext::new(&mut event_queue, &mut self.config);
let mut context = UpdateContext::new(&mut event_queue);

let update =
Self::update_all(self.root.as_child(), &mut context, event);
Expand Down Expand Up @@ -184,19 +174,3 @@ impl View {
})
}
}

/// Settings that control the behavior of the view
#[derive(Debug)]
struct ViewConfig {
/// Should templates be rendered inline in the UI, or should we show the
/// raw text?
preview_templates: bool,
}

impl Default for ViewConfig {
fn default() -> Self {
Self {
preview_templates: true,
}
}
}
11 changes: 6 additions & 5 deletions src/tui/view/common/modal.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::tui::{
input::Action,
view::{
draw::{Draw, DrawContext},
draw::Draw,
event::{Event, EventHandler, Update, UpdateContext},
util::centered_rect,
Component,
Expand All @@ -10,6 +10,7 @@ use crate::tui::{
use ratatui::{
prelude::{Constraint, Rect},
widgets::{Block, BorderType, Borders, Clear},
Frame,
};
use std::{collections::VecDeque, ops::DerefMut};
use tracing::trace;
Expand Down Expand Up @@ -131,7 +132,7 @@ impl EventHandler for ModalQueue {
}

impl Draw for ModalQueue {
fn draw(&self, context: &mut DrawContext, _: (), area: Rect) {
fn draw(&self, frame: &mut Frame, _: (), area: Rect) {
if let Some(modal) = self.queue.front() {
let (width, height) = modal.dimensions();

Expand All @@ -150,11 +151,11 @@ impl Draw for ModalQueue {
let inner_area = block.inner(area);

// Draw the outline of the modal
context.frame.render_widget(Clear, area);
context.frame.render_widget(block, area);
frame.render_widget(Clear, area);
frame.render_widget(block, area);

// Render the actual content
modal.draw(context, (), inner_area);
modal.draw(frame, (), inner_area);
}
}
}
Expand Down
Loading

0 comments on commit 71776cf

Please sign in to comment.