fix: harden timer and release flow
This commit is contained in:
@@ -58,7 +58,8 @@ jobs:
|
|||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
binary="gosleep-timer-${version}-linux-amd64"
|
binary="gosleep-timer-${version}-linux-amd64"
|
||||||
cp target/release/gosleep-timer "dist/${binary}"
|
cp target/release/gosleep-timer "dist/${binary}"
|
||||||
sha256sum "dist/${binary}" > "dist/${binary}.sha256"
|
cd dist
|
||||||
|
sha256sum "${binary}" > "${binary}.sha256"
|
||||||
|
|
||||||
- name: Create Gitea release
|
- name: Create Gitea release
|
||||||
id: release
|
id: release
|
||||||
@@ -100,7 +101,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
release_url="https://gitea.forust.xyz/api/v1/repos/forust/gosleep/releases"
|
release_url="https://gitea.forust.xyz/api/v1/repos/forust/gosleep/releases"
|
||||||
release_id="${{ steps.release.outputs.release_id }}"
|
release_id="${{ steps.release.outputs.release_id }}"
|
||||||
for asset in dist/gosleep-timer-* dist/gosleep-timer-*.sha256; do
|
for asset in dist/gosleep-timer-*; do
|
||||||
name="$(basename "$asset")"
|
name="$(basename "$asset")"
|
||||||
release="$(curl -fsS \
|
release="$(curl -fsS \
|
||||||
"${release_url}/${release_id}" \
|
"${release_url}/${release_id}" \
|
||||||
|
|||||||
@@ -73,20 +73,21 @@ test "$(cat VERSION)" = "$(cargo metadata --no-deps --format-version 1 | jq -r '
|
|||||||
Release workflow is tag-based:
|
Release workflow is tag-based:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git tag -a v1.1.1 -m "gosleep-timer v1.1.1"
|
git tag -a vMAJOR.MINOR.PATCH -m "gosleep-timer vMAJOR.MINOR.PATCH"
|
||||||
git push origin main
|
git push origin main
|
||||||
git push origin v1.1.1
|
git push origin vMAJOR.MINOR.PATCH
|
||||||
```
|
```
|
||||||
|
|
||||||
The Gitea release workflow:
|
The Gitea release workflow:
|
||||||
|
|
||||||
|
- runs formatting, tests, clippy, and YAML lint before building
|
||||||
- builds `target/release/gosleep-timer`
|
- builds `target/release/gosleep-timer`
|
||||||
- creates `gosleep-timer-<version>-linux-amd64.tar.gz`
|
- copies `gosleep-timer-<version>-linux-amd64`
|
||||||
- writes a `.sha256`
|
- writes a `.sha256`
|
||||||
- creates or reuses the matching Gitea release
|
- creates or reuses the matching Gitea release
|
||||||
- replaces release assets with matching names
|
- replaces release assets with matching names
|
||||||
|
|
||||||
The workflow uses the built-in `GITEA_TOKEN` and declares `permissions: contents: write`.
|
The workflow exposes `${{ secrets.GITEA_TOKEN }}` as `GITEA_TOKEN` and declares `permissions: contents: write`.
|
||||||
|
|
||||||
## Git Remote
|
## Git Remote
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ install -Dm755 target/release/gosleep-timer ~/.local/bin/gosleep-timer
|
|||||||
|
|
||||||
### From release artifact
|
### From release artifact
|
||||||
|
|
||||||
Download `gosleep-timer-<version>-linux-amd64.tar.gz` from the Gitea release page, then:
|
Download `gosleep-timer-<version>-linux-amd64` and its `.sha256` from the Gitea release page, then:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tar -xzf gosleep-timer-<version>-linux-amd64.tar.gz
|
sha256sum -c gosleep-timer-<version>-linux-amd64.sha256
|
||||||
install -Dm755 gosleep-timer ~/.local/bin/gosleep-timer
|
install -Dm755 gosleep-timer-<version>-linux-amd64 ~/.local/bin/gosleep-timer
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -197,12 +197,11 @@ Required runner tools:
|
|||||||
- Rust stable toolchain with `cargo`, `rustfmt`, and `clippy`
|
- Rust stable toolchain with `cargo`, `rustfmt`, and `clippy`
|
||||||
- `curl`
|
- `curl`
|
||||||
- `jq`
|
- `jq`
|
||||||
- `tar`
|
|
||||||
- `sha256sum`
|
- `sha256sum`
|
||||||
|
|
||||||
Release authentication:
|
Release authentication:
|
||||||
|
|
||||||
- The workflow uses Gitea Actions' built-in `GITEA_TOKEN`.
|
- The workflow exposes Gitea Actions' built-in `${{ secrets.GITEA_TOKEN }}` as `GITEA_TOKEN`.
|
||||||
- The workflow declares `permissions: contents: write` so the token can create releases and upload assets.
|
- The workflow declares `permissions: contents: write` so the token can create releases and upload assets.
|
||||||
|
|
||||||
See [RELEASING.md](RELEASING.md) for the full release checklist.
|
See [RELEASING.md](RELEASING.md) for the full release checklist.
|
||||||
|
|||||||
+7
-7
@@ -36,7 +36,6 @@ https://gitea.forust.xyz/forust/gosleep.git
|
|||||||
- `clippy`
|
- `clippy`
|
||||||
- `curl`
|
- `curl`
|
||||||
- `jq`
|
- `jq`
|
||||||
- `tar`
|
|
||||||
- `sha256sum`
|
- `sha256sum`
|
||||||
- Docker, if using the YAML lint job as written
|
- Docker, if using the YAML lint job as written
|
||||||
4. Confirm Actions job token permissions allow `contents: write`.
|
4. Confirm Actions job token permissions allow `contents: write`.
|
||||||
@@ -44,10 +43,10 @@ https://gitea.forust.xyz/forust/gosleep.git
|
|||||||
|
|
||||||
## Version Bump Checklist
|
## Version Bump Checklist
|
||||||
|
|
||||||
For a patch bump, for example `1.1.1` to `1.1.2`:
|
For a patch bump, for example `1.1.3` to `1.1.4`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
version=1.1.2
|
version=1.1.4
|
||||||
```
|
```
|
||||||
|
|
||||||
Update:
|
Update:
|
||||||
@@ -70,10 +69,10 @@ cargo build --release --locked
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git status --short
|
git status --short
|
||||||
git commit -am "chore: release v1.1.2"
|
git commit -am "chore: release v${version}"
|
||||||
git tag -a v1.1.2 -m "gosleep-timer v1.1.2"
|
git tag -a "v${version}" -m "gosleep-timer v${version}"
|
||||||
git push origin main
|
git push origin main
|
||||||
git push origin v1.1.2
|
git push origin "v${version}"
|
||||||
```
|
```
|
||||||
|
|
||||||
The `.gitea/workflows/release.yaml` workflow will:
|
The `.gitea/workflows/release.yaml` workflow will:
|
||||||
@@ -98,5 +97,6 @@ cargo build --release --locked
|
|||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
binary="gosleep-timer-${version}-linux-amd64"
|
binary="gosleep-timer-${version}-linux-amd64"
|
||||||
cp target/release/gosleep-timer "dist/${binary}"
|
cp target/release/gosleep-timer "dist/${binary}"
|
||||||
sha256sum "dist/${binary}" > "dist/${binary}.sha256"
|
cd dist
|
||||||
|
sha256sum "${binary}" > "${binary}.sha256"
|
||||||
```
|
```
|
||||||
|
|||||||
+166
-22
@@ -242,6 +242,7 @@ fn ensure_config(path: &PathBuf) -> Result<Config> {
|
|||||||
let data = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
|
let data = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
|
||||||
let mut config: Config = serde_yaml::from_str(&data).context("parse config")?;
|
let mut config: Config = serde_yaml::from_str(&data).context("parse config")?;
|
||||||
normalize_config(&mut config);
|
normalize_config(&mut config);
|
||||||
|
validate_config(&config)?;
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,14 +522,31 @@ fn run_timer(
|
|||||||
thread::sleep(Duration::from_millis(250));
|
thread::sleep(Duration::from_millis(250));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if stop.load(Ordering::Relaxed) {
|
||||||
|
if let Some(output) = output.as_deref_mut() {
|
||||||
|
write_timer_line(output, "stopped");
|
||||||
|
}
|
||||||
|
bail!("timer stopped");
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(output) = output.as_deref_mut() {
|
if let Some(output) = output.as_deref_mut() {
|
||||||
write_timer_line(output, "finished");
|
write_timer_line(output, "finished");
|
||||||
}
|
}
|
||||||
|
let mut failures = Vec::new();
|
||||||
for command in build_action_commands(config) {
|
for command in build_action_commands(config) {
|
||||||
if let Some(output) = output.as_deref_mut() {
|
if let Some(output) = output.as_deref_mut() {
|
||||||
write_timer_line(output, &format!("run: {}", command.display()));
|
write_timer_line(output, &format!("run: {}", command.display()));
|
||||||
}
|
}
|
||||||
run_command(&command, mode).with_context(|| format!("{} failed", command.label))?;
|
if let Err(err) = run_command(&command, mode) {
|
||||||
|
failures.push(format!("{} failed: {err}", command.label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !failures.is_empty() {
|
||||||
|
bail!(
|
||||||
|
"{} action(s) failed: {}",
|
||||||
|
failures.len(),
|
||||||
|
failures.join("; ")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -562,15 +580,18 @@ fn run_command(command: &ActionCommand, mode: RunMode) -> Result<()> {
|
|||||||
|
|
||||||
fn run_tui(path: PathBuf, config: Config) -> Result<()> {
|
fn run_tui(path: PathBuf, config: Config) -> Result<()> {
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
let mut stdout = io::stdout();
|
let result = (|| {
|
||||||
execute!(stdout, EnterAlternateScreen)?;
|
let mut stdout = io::stdout();
|
||||||
let backend = CrosstermBackend::new(stdout);
|
execute!(stdout, EnterAlternateScreen)?;
|
||||||
let mut terminal = Terminal::new(backend)?;
|
let backend = CrosstermBackend::new(stdout);
|
||||||
let mut app = App::new(path, config);
|
let mut terminal = Terminal::new(backend)?;
|
||||||
let result = app.run(&mut terminal);
|
let mut app = App::new(path, config);
|
||||||
|
let result = app.run(&mut terminal);
|
||||||
|
terminal.show_cursor().ok();
|
||||||
|
result
|
||||||
|
})();
|
||||||
disable_raw_mode().ok();
|
disable_raw_mode().ok();
|
||||||
execute!(terminal.backend_mut(), LeaveAlternateScreen).ok();
|
execute!(io::stdout(), LeaveAlternateScreen).ok();
|
||||||
terminal.show_cursor().ok();
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,7 +657,8 @@ impl App {
|
|||||||
self.focus = self.focus.saturating_sub(1)
|
self.focus = self.focus.saturating_sub(1)
|
||||||
}
|
}
|
||||||
KeyCode::Down | KeyCode::Char('j') => {
|
KeyCode::Down | KeyCode::Char('j') => {
|
||||||
self.focus = (self.focus + 1).min(FIELDS.len() - 1);
|
let last = self.visible_fields().len().saturating_sub(1);
|
||||||
|
self.focus = (self.focus + 1).min(last);
|
||||||
}
|
}
|
||||||
KeyCode::Char(' ') | KeyCode::Enter => self.activate()?,
|
KeyCode::Char(' ') | KeyCode::Enter => self.activate()?,
|
||||||
KeyCode::Char('s') => self.save(),
|
KeyCode::Char('s') => self.save(),
|
||||||
@@ -747,8 +769,10 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_fields(&mut self, frame: &mut Frame, area: Rect) {
|
fn draw_fields(&mut self, frame: &mut Frame, area: Rect) {
|
||||||
|
let fields = self.visible_fields();
|
||||||
let visible = area.height.saturating_sub(2) as usize;
|
let visible = area.height.saturating_sub(2) as usize;
|
||||||
self.offset = self.offset.min(FIELDS.len().saturating_sub(visible));
|
self.focus = self.focus.min(fields.len().saturating_sub(1));
|
||||||
|
self.offset = self.offset.min(fields.len().saturating_sub(visible));
|
||||||
if self.focus < self.offset {
|
if self.focus < self.offset {
|
||||||
self.offset = self.focus;
|
self.offset = self.focus;
|
||||||
}
|
}
|
||||||
@@ -756,7 +780,7 @@ impl App {
|
|||||||
self.offset = self.focus.saturating_sub(visible.saturating_sub(1));
|
self.offset = self.focus.saturating_sub(visible.saturating_sub(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
let items = FIELDS
|
let items = fields
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.skip(self.offset)
|
.skip(self.offset)
|
||||||
@@ -809,11 +833,13 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn activate(&mut self) -> Result<()> {
|
fn activate(&mut self) -> Result<()> {
|
||||||
match FIELDS[self.focus].kind {
|
let Some(field) = self.focused_field() else {
|
||||||
FieldKind::Bool => self.toggle_bool(FIELDS[self.focus].key),
|
return Ok(());
|
||||||
FieldKind::Cycle(options) => self.cycle(FIELDS[self.focus].key, options),
|
};
|
||||||
|
match field.kind {
|
||||||
|
FieldKind::Bool => self.toggle_bool(field.key),
|
||||||
|
FieldKind::Cycle(options) => self.cycle(field.key, options),
|
||||||
FieldKind::Edit | FieldKind::Int | FieldKind::Csv | FieldKind::Semi => {
|
FieldKind::Edit | FieldKind::Int | FieldKind::Csv | FieldKind::Semi => {
|
||||||
let field = FIELDS[self.focus];
|
|
||||||
self.edit = Some(EditState {
|
self.edit = Some(EditState {
|
||||||
field,
|
field,
|
||||||
value: self.field_value(&field),
|
value: self.field_value(&field),
|
||||||
@@ -915,8 +941,10 @@ impl App {
|
|||||||
fn stop_timer(&mut self) {
|
fn stop_timer(&mut self) {
|
||||||
if let Some(timer) = self.timer.take() {
|
if let Some(timer) = self.timer.take() {
|
||||||
timer.stop.store(true, Ordering::Relaxed);
|
timer.stop.store(true, Ordering::Relaxed);
|
||||||
|
self.apply_timer_result(timer.handle.join());
|
||||||
|
} else {
|
||||||
|
self.status = "timer stopped".to_string();
|
||||||
}
|
}
|
||||||
self.status = "timer stopped".to_string();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reap_timer(&mut self) {
|
fn reap_timer(&mut self) {
|
||||||
@@ -926,11 +954,18 @@ impl App {
|
|||||||
.is_some_and(|timer| timer.handle.is_finished())
|
.is_some_and(|timer| timer.handle.is_finished())
|
||||||
{
|
{
|
||||||
let timer = self.timer.take().expect("timer exists");
|
let timer = self.timer.take().expect("timer exists");
|
||||||
match timer.handle.join() {
|
self.apply_timer_result(timer.handle.join());
|
||||||
Ok(Ok(())) => self.status = "timer finished".to_string(),
|
}
|
||||||
Ok(Err(err)) => self.status = format!("timer failed: {err}"),
|
}
|
||||||
Err(_) => self.status = "timer failed: thread panicked".to_string(),
|
|
||||||
|
fn apply_timer_result(&mut self, result: thread::Result<Result<()>>) {
|
||||||
|
match result {
|
||||||
|
Ok(Ok(())) => self.status = "timer finished".to_string(),
|
||||||
|
Ok(Err(err)) if err.to_string() == "timer stopped" => {
|
||||||
|
self.status = "timer stopped".to_string()
|
||||||
}
|
}
|
||||||
|
Ok(Err(err)) => self.status = format!("timer failed: {err}"),
|
||||||
|
Err(_) => self.status = "timer failed: thread panicked".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1005,6 +1040,14 @@ impl App {
|
|||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
*current = options[(index + 1) % options.len()].to_string();
|
*current = options[(index + 1) % options.len()].to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn visible_fields(&self) -> Vec<&'static Field> {
|
||||||
|
visible_fields(&self.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focused_field(&self) -> Option<Field> {
|
||||||
|
self.visible_fields().get(self.focus).map(|field| **field)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -1024,7 +1067,7 @@ enum FieldKind {
|
|||||||
Semi,
|
Semi,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
enum FieldKey {
|
enum FieldKey {
|
||||||
Duration,
|
Duration,
|
||||||
WorkspaceEnabled,
|
WorkspaceEnabled,
|
||||||
@@ -1127,6 +1170,33 @@ const FIELDS: &[Field] = &[
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
fn visible_fields(config: &Config) -> Vec<&'static Field> {
|
||||||
|
FIELDS
|
||||||
|
.iter()
|
||||||
|
.filter(|field| field_visible(config, field.key))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_visible(config: &Config, key: FieldKey) -> bool {
|
||||||
|
match key {
|
||||||
|
FieldKey::WorkspaceBackend | FieldKey::WorkspaceNumber => config.actions.workspace.enabled,
|
||||||
|
FieldKey::MediaAction => config.actions.media.enabled,
|
||||||
|
FieldKey::BrightnessValue => config.actions.brightness.enabled,
|
||||||
|
FieldKey::LockCommand => config.actions.lock.enabled,
|
||||||
|
FieldKey::KillProcesses => config.actions.kill.enabled,
|
||||||
|
FieldKey::CustomCommands => config.actions.custom.enabled,
|
||||||
|
FieldKey::Duration
|
||||||
|
| FieldKey::WorkspaceEnabled
|
||||||
|
| FieldKey::MediaEnabled
|
||||||
|
| FieldKey::BrightnessEnabled
|
||||||
|
| FieldKey::MuteEnabled
|
||||||
|
| FieldKey::LockEnabled
|
||||||
|
| FieldKey::KillEnabled
|
||||||
|
| FieldKey::PowerMode
|
||||||
|
| FieldKey::CustomEnabled => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_off(value: bool) -> String {
|
fn on_off(value: bool) -> String {
|
||||||
if value { "on" } else { "off" }.to_string()
|
if value { "on" } else { "off" }.to_string()
|
||||||
}
|
}
|
||||||
@@ -1297,6 +1367,41 @@ mod tests {
|
|||||||
assert!(text.contains("finished"));
|
assert!(text.contains("finished"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_timer_attempts_all_actions_before_returning_failures() {
|
||||||
|
let stop = Arc::new(AtomicBool::new(false));
|
||||||
|
let mut config = quiet_config("1ns");
|
||||||
|
config.actions.custom.enabled = true;
|
||||||
|
config.actions.custom.commands = vec!["false".to_string(), "true".to_string()];
|
||||||
|
let mut output = Vec::new();
|
||||||
|
|
||||||
|
let error = run_timer(&config, &stop, RunMode::Cli, Some(&mut output)).unwrap_err();
|
||||||
|
|
||||||
|
let text = String::from_utf8(output).unwrap();
|
||||||
|
assert!(text.contains("run: false"));
|
||||||
|
assert!(text.contains("run: true"));
|
||||||
|
assert!(error.to_string().contains("1 action(s) failed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ensure_config_validates_loaded_config() {
|
||||||
|
let path = std::env::temp_dir().join(format!(
|
||||||
|
"gosleep-timer-invalid-{}-{}.yaml",
|
||||||
|
std::process::id(),
|
||||||
|
"power"
|
||||||
|
));
|
||||||
|
fs::write(
|
||||||
|
&path,
|
||||||
|
"duration: 25m\nactions:\n power:\n mode: suspend\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let error = ensure_config(&path).unwrap_err();
|
||||||
|
fs::remove_file(path).ok();
|
||||||
|
|
||||||
|
assert!(error.to_string().contains("invalid power mode"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn timer_header_text_compacts_for_narrow_widths() {
|
fn timer_header_text_compacts_for_narrow_widths() {
|
||||||
let remaining = Duration::from_secs(65);
|
let remaining = Duration::from_secs(65);
|
||||||
@@ -1323,6 +1428,45 @@ mod tests {
|
|||||||
assert_eq!(format_gauge_label(1.0), "100%");
|
assert_eq!(format_gauge_label(1.0), "100%");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn visible_fields_hide_disabled_action_settings() {
|
||||||
|
let mut config = quiet_config("25m");
|
||||||
|
|
||||||
|
let keys = visible_field_keys(&config);
|
||||||
|
assert!(keys.contains(&FieldKey::Duration));
|
||||||
|
assert!(keys.contains(&FieldKey::MediaEnabled));
|
||||||
|
assert!(!keys.contains(&FieldKey::MediaAction));
|
||||||
|
assert!(!keys.contains(&FieldKey::WorkspaceBackend));
|
||||||
|
assert!(!keys.contains(&FieldKey::WorkspaceNumber));
|
||||||
|
assert!(!keys.contains(&FieldKey::BrightnessValue));
|
||||||
|
assert!(!keys.contains(&FieldKey::LockCommand));
|
||||||
|
assert!(!keys.contains(&FieldKey::KillProcesses));
|
||||||
|
assert!(!keys.contains(&FieldKey::CustomCommands));
|
||||||
|
|
||||||
|
config.actions.media.enabled = true;
|
||||||
|
config.actions.workspace.enabled = true;
|
||||||
|
config.actions.brightness.enabled = true;
|
||||||
|
config.actions.lock.enabled = true;
|
||||||
|
config.actions.kill.enabled = true;
|
||||||
|
config.actions.custom.enabled = true;
|
||||||
|
|
||||||
|
let keys = visible_field_keys(&config);
|
||||||
|
assert!(keys.contains(&FieldKey::MediaAction));
|
||||||
|
assert!(keys.contains(&FieldKey::WorkspaceBackend));
|
||||||
|
assert!(keys.contains(&FieldKey::WorkspaceNumber));
|
||||||
|
assert!(keys.contains(&FieldKey::BrightnessValue));
|
||||||
|
assert!(keys.contains(&FieldKey::LockCommand));
|
||||||
|
assert!(keys.contains(&FieldKey::KillProcesses));
|
||||||
|
assert!(keys.contains(&FieldKey::CustomCommands));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visible_field_keys(config: &Config) -> Vec<FieldKey> {
|
||||||
|
visible_fields(config)
|
||||||
|
.into_iter()
|
||||||
|
.map(|field| field.key)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn dummy_timer_state() -> TimerState {
|
fn dummy_timer_state() -> TimerState {
|
||||||
let stop = Arc::new(AtomicBool::new(false));
|
let stop = Arc::new(AtomicBool::new(false));
|
||||||
let handle = thread::spawn(|| Ok(()));
|
let handle = thread::spawn(|| Ok(()));
|
||||||
|
|||||||
Reference in New Issue
Block a user